diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index a17f5632f9ad..bc9cdb1b3b9e 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -33,7 +33,7 @@ jobs: echo "pr=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" echo "repo=${{ github.event.pull_request.head.repo.full_name }}" >> "$GITHUB_OUTPUT" fi - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: repository: ${{ steps.get-vars.outputs.repo }} ref: ${{ steps.get-vars.outputs.branch }} diff --git a/.github/workflows/assembleFlavors.yml b/.github/workflows/assembleFlavors.yml index 165c7f720a6f..a334359beae6 100644 --- a/.github/workflows/assembleFlavors.yml +++ b/.github/workflows/assembleFlavors.yml @@ -19,7 +19,7 @@ jobs: matrix: flavor: [ Generic, Gplay, Huawei ] steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v3 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v3 - name: set up JDK 17 uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3 with: diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 3151bfac84d8..ddb198952505 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -19,7 +19,7 @@ jobs: matrix: task: [ detekt, spotlessKotlinCheck ] steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v3 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v3 - name: Set up JDK 17 uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3 with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3ea950d1c13b..369b7acfc146 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -12,6 +12,10 @@ on: permissions: contents: read +concurrency: + group: code-ql-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + jobs: analyze: name: Analyze @@ -26,13 +30,13 @@ jobs: language: [ 'java' ] steps: - name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Set Swap Space uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c # v1.0 with: swap-size-gb: 10 - name: Initialize CodeQL - uses: github/codeql-action/init@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3 + uses: github/codeql-action/init@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4 with: languages: ${{ matrix.language }} - name: Set up JDK 17 @@ -46,4 +50,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@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3 + uses: github/codeql-action/analyze@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4 diff --git a/.github/workflows/command-rebase.yml b/.github/workflows/command-rebase.yml index 8c215c1c4ca6..60876df35457 100644 --- a/.github/workflows/command-rebase.yml +++ b/.github/workflows/command-rebase.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Add reaction on start - uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 + uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0 with: token: ${{ secrets.COMMAND_BOT_PAT }} repository: ${{ github.event.repository.full_name }} @@ -31,7 +31,7 @@ jobs: reaction-type: "+1" - name: Checkout the latest code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 0 token: ${{ secrets.COMMAND_BOT_PAT }} @@ -42,7 +42,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.COMMAND_BOT_PAT }} - name: Add reaction on failure - uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 + uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0 if: failure() with: token: ${{ secrets.COMMAND_BOT_PAT }} diff --git a/.github/workflows/detectWrongSettings.yml b/.github/workflows/detectWrongSettings.yml index 599e7d06b4c5..9d6dce59a20c 100644 --- a/.github/workflows/detectWrongSettings.yml +++ b/.github/workflows/detectWrongSettings.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v3 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v3 - name: Set up JDK 17 uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3 with: diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 45a120cb4ed1..1ecd60deb21a 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -18,5 +18,5 @@ jobs: name: "Validation" runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: gradle/wrapper-validation-action@56b90f209b02bf6d1deae490e9ef18b21a389cd4 # v1.1.0 diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 8f9090e6cf50..8a6f9e6cd7ad 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -19,7 +19,7 @@ jobs: - name: Check if secrets are available run: echo "::set-output name=ok::${{ secrets.KS_PASS != '' }}" id: check-secrets - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v3 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v3 if: ${{ steps.check-secrets.outputs.ok == 'true' }} - name: set up JDK 17 uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index f54c98fedb6d..bd873fbb8143 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -24,12 +24,12 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@483ef80eb98fb506c348f7d62e28055e49fe2398 # v2.3.0 + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 with: results_file: results.sarif results_format: sarif @@ -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@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3 + uses: github/codeql-action/upload-sarif@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2.22.4 with: sarif_file: results.sarif diff --git a/.github/workflows/screenShotTest.yml b/.github/workflows/screenShotTest.yml index 9b768312963c..e036d66e6ec5 100644 --- a/.github/workflows/screenShotTest.yml +++ b/.github/workflows/screenShotTest.yml @@ -22,7 +22,7 @@ jobs: color: [ blue ] api-level: [ 27 ] steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v3 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v3 - name: Gradle cache uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3 diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index f7b750515f89..12a5a6da2b64 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -18,7 +18,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Set up JDK 17 uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index ebc537ffa5c9..5d6df215c09e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,30 @@ -## 3.24.1 (February 21, 2022) +## 3.26.0 (September 16, 2023) + +- image editing +- image details, with map +- show other Nextcloud apps + +Minimum: NC 16 Server, Android 6.0 Marshmallow + +For a full list, please see https://github.com/nextcloud/android/milestone/84 + +## 3.25.0 (June 13, 2023) + +- show Groupfolder +- Tag in file listing + +Minimum: NC 16 Server, Android 6.0 Marshmallow + +For a full list, please see https://github.com/nextcloud/android/milestone/81 + +## 3.24.1 (February 21, 2023) - Fix crash in previous version when connecting to old server versions Minimum: NC 16 Server, Android 6.0 Marshmallow +For a full list, please see https://github.com/nextcloud/android/milestone/80 + ## 3.24.0 (February 13, 2023) - Several performance optimizations by @starypatyk diff --git a/README.md b/README.md index 5bf764cecdf2..cdf43994ab1e 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,17 @@ height="80">](https://f-droid.org/packages/com.nextcloud.client/) ## How to contribute :rocket: -If you want to [contribute](https://nextcloud.com/contribute/) to Nextcloud, you are very welcome: - -* our forum at https://help.nextcloud.com -* for translations of the app on [Transifex](https://app.transifex.com/nextcloud/nextcloud/android/) -* opening issues and PRs (including a corresponding issue) +If you want to [contribute](https://nextcloud.com/contribute/) to the Nextcloud Android client app, there are many ways to help whether or not you are a coder: + +* helping out other users on our forum at https://help.nextcloud.com +* providing translations of the app on [Transifex](https://app.transifex.com/nextcloud/nextcloud/android/) +* reporting problems / suggesting enhancements by [opening new issues](https://github.com/nextcloud/android/issues/new/choose) +* implementing proposed bug fixes and enhancement ideas by submitting PRs (associated with a corresponding issue preferably) +* reviewing [pull requests](https://github.com/nextcloud/android/pulls) and providing feedback on code, implementation, and functionality +* installing and testing [pull request builds](https://github.com/nextcloud/android/pulls), [daily/dev builds](https://github.com/nextcloud/android#development-version-hammer), or [RCs/release candidate builds](https://github.com/nextcloud/android/releases) +* enhancing Admin, User, or Developer [documentation](https://github.com/nextcloud/documentation/) +* hitting hard on the latest stable release by testing fundamental features and evaluating the user experience +* proactively getting familiar with [how to gather debug logs](https://github.com/nextcloud/android#getting-debug-info-via-logcat-mag) from your devices (so that you are prepared to provide a detailed report if you encounter a problem with the app in the future) ## Contribution Guidelines & License :scroll: @@ -38,7 +44,7 @@ More information on how to contribute: ## Start contributing :hammer\_and\_wrench: Make sure you read [SETUP.md](https://github.com/nextcloud/android/blob/master/SETUP.md) and [CONTRIBUTING.md](https://github.com/nextcloud/android/blob/master/CONTRIBUTING.md) before you start working on this project. But basically: fork this repository and contribute back using pull requests to the master branch. -Easy starting points are also reviewing [pull requests](https://github.com/nextcloud/android/pulls) and working on [starter issues](https://github.com/nextcloud/android/issues?q=is%3Aopen+is%3Aissue+label%3A%22starter+issue%22). +Easy starting points are also reviewing [pull requests](https://github.com/nextcloud/android/pulls) and working on [starter issues](https://github.com/nextcloud/android/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). ### Getting debug info via logcat :mag: diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth.png index 47172a3da27b..97e0219c3302 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting.png index 8ad1818f865e..57b78febd9c2 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew.png index e51fdd8144d0..bf4f56484ee1 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles.png index 336694046bc1..df72127e511a 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference.png index 26c12663b702..f450422215b5 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog.png index 658eafbc80e7..d4dfd20689b3 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog.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 c4bd33b4abd2..e451319a42a4 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 6681374612c1..caa860dc2b97 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 1ef759d4f6dc..ff264659676c 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 6681374612c1..caa860dc2b97 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/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testStoragePermissionDialog.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testStoragePermissionDialog.png index 065c11e16351..7aae97506053 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testStoragePermissionDialog.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testStoragePermissionDialog.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment.png index 01ad7143ffef..32a31a678acb 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailDetailsFragment.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailDetailsFragment.png index 01d6675377f2..e853f17b9131 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailDetailsFragment.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailDetailsFragment.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 9549951b3471..68bd9cc9d452 100644 --- a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java +++ b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java @@ -314,7 +314,7 @@ protected Activity getCurrentActivity() { return currentActivity; } - protected void shortSleep() { + protected static void shortSleep() { try { Thread.sleep(2000); } catch (InterruptedException e) { diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java index fd2e206e7f2c..94f54f69615e 100644 --- a/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java +++ b/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java @@ -135,10 +135,20 @@ public static void deleteAllFilesOnServer() { .isSuccess()); } - assertTrue(new RemoveFileRemoteOperation(remoteFile.getRemotePath()) - .execute(client) - .isSuccess() - ); + boolean removeResult = false; + for (int i = 0; i < 5; i++) { + removeResult = new RemoveFileRemoteOperation(remoteFile.getRemotePath()) + .execute(client) + .isSuccess(); + + if (removeResult) { + break; + } + + shortSleep(); + } + + assertTrue(removeResult); } } } diff --git a/app/src/androidTest/java/com/owncloud/android/UploadIT.java b/app/src/androidTest/java/com/owncloud/android/UploadIT.java index e93b328f0865..7b86de769169 100644 --- a/app/src/androidTest/java/com/owncloud/android/UploadIT.java +++ b/app/src/androidTest/java/com/owncloud/android/UploadIT.java @@ -41,7 +41,6 @@ import com.owncloud.android.operations.UploadFileOperation; import com.owncloud.android.utils.FileStorageUtils; -import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -106,29 +105,6 @@ public void before() throws IOException { createDummyFiles(); } - @After - public void after() { - RemoteOperationResult result = new RefreshFolderOperation(getStorageManager().getFileByPath("/"), - System.currentTimeMillis() / 1000L, - false, - true, - getStorageManager(), - user, - targetContext) - .execute(client); - - // cleanup only if folder exists - if (result.isSuccess() && getStorageManager().getFileByDecryptedRemotePath(FOLDER) != null) { - new RemoveFileOperation(getStorageManager().getFileByDecryptedRemotePath(FOLDER), - false, - user, - false, - targetContext, - getStorageManager()) - .execute(client); - } - } - @Test public void testEmptyUpload() { OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/empty.txt", @@ -529,8 +505,8 @@ public void testMetadata() throws IOException, AccountUtils.AccountNotFoundExcep assertNotNull(ocFile); assertEquals(remotePath, ocFile.getRemotePath()); - assertEquals(new ImageDimension(451f, 529f), ocFile.getImageDimension()); - assertEquals(new GeoLocation(49.99679166666667, 8.67198611111111), ocFile.getGeoLocation()); + assertEquals(new ImageDimension(300f, 200f), ocFile.getImageDimension()); + assertEquals(new GeoLocation(64, -46), ocFile.getGeoLocation()); } private void verifyStoragePath(OCFile file) { diff --git a/app/src/androidTest/java/com/owncloud/android/ui/LoginIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/LoginIT.kt index 67ba9cd40ff6..3bf0378c01bd 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/LoginIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/LoginIT.kt @@ -20,6 +20,7 @@ */ package com.owncloud.android.ui +import android.os.Build import androidx.test.core.app.ActivityScenario import androidx.test.espresso.Espresso import androidx.test.espresso.action.ViewActions @@ -65,7 +66,7 @@ class LoginIT : AbstractIT() { * The CI/CD pipeline is encountering issues related to the Android version for this functionality. * Therefore the test will only be executed on Android versions 10 and above. */ - @SdkSuppress(minSdkVersion = 29) + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) fun login() { val arguments = InstrumentationRegistry.getArguments() val baseUrl = arguments.getString("TEST_SERVER_URL")!! diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailFragmentStaticServerIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailFragmentStaticServerIT.kt index 5f6883e1ab67..71213ff5928c 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailFragmentStaticServerIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailFragmentStaticServerIT.kt @@ -73,14 +73,19 @@ class FileDetailFragmentStaticServerIT : AbstractIT() { @Test @ScreenshotTest fun showFileDetailDetailsFragment() { - val sut = testActivityRule.launchActivity(null) - sut.addFragment(ImageDetailFragment.newInstance(oCFile, user)) + val activity = testActivityRule.launchActivity(null) + val sut = ImageDetailFragment.newInstance(oCFile, user) + activity.addFragment(sut) - waitForIdleSync() - shortSleep() shortSleep() shortSleep() - screenshot(sut) + waitForIdleSync() + + activity.runOnUiThread { + sut.hideMap() + } + + screenshot(activity) } @Test @@ -182,6 +187,7 @@ class FileDetailFragmentStaticServerIT : AbstractIT() { waitForIdleSync() activity.runOnUiThread { + sut.fileDetailActivitiesFragment.disableLoadingActivities() sut .fileDetailActivitiesFragment .setErrorContent(targetContext.resources.getString(R.string.file_detail_activity_error)) diff --git a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java index d776c042a9db..5039fb427441 100644 --- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java +++ b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java @@ -92,7 +92,6 @@ import com.owncloud.android.ui.dialog.LoadingDialog; import com.owncloud.android.ui.dialog.LocalStoragePathPickerDialogFragment; import com.owncloud.android.ui.dialog.MultipleAccountsDialog; -import com.owncloud.android.ui.dialog.NoteDialogFragment; import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment; import com.owncloud.android.ui.dialog.RenameFileDialogFragment; import com.owncloud.android.ui.dialog.RenamePublicShareDialogFragment; @@ -404,9 +403,6 @@ abstract class ComponentsModule { @ContributesAndroidInjector abstract Migrations migrations(); - @ContributesAndroidInjector - abstract NoteDialogFragment noteDialogFragment(); - @ContributesAndroidInjector abstract NotificationWork notificationWork(); diff --git a/app/src/main/java/com/nextcloud/client/jobs/FilesExportWork.kt b/app/src/main/java/com/nextcloud/client/jobs/FilesExportWork.kt index 427b9f605d7b..d8a417513be0 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/FilesExportWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/FilesExportWork.kt @@ -29,7 +29,6 @@ import android.content.ContentResolver import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_NEW_TASK -import android.graphics.BitmapFactory import androidx.core.app.NotificationCompat import androidx.work.Worker import androidx.work.WorkerParameters @@ -69,9 +68,7 @@ class FilesExportWork( val successfulExports = exportFiles(fileIDs) - // show notification showSuccessNotification(successfulExports) - return Result.success() } @@ -105,7 +102,13 @@ class FilesExportWork( @Throws(IllegalStateException::class) private fun exportFile(ocFile: OCFile) { - FileExportUtils().exportFile(ocFile.fileName, ocFile.mimeType, contentResolver, ocFile, null) + FileExportUtils().exportFile( + ocFile.fileName, + ocFile.mimeType, + contentResolver, + ocFile, + null + ) } private fun downloadFile(ocFile: OCFile) { @@ -119,19 +122,16 @@ class FilesExportWork( } private fun showErrorNotification(successfulExports: Int) { - if (successfulExports == 0) { - showNotification( - appContext.resources.getQuantityString(R.plurals.export_failed, successfulExports, successfulExports) - ) + val message = if (successfulExports == 0) { + appContext.resources.getQuantityString(R.plurals.export_failed, successfulExports, successfulExports) } else { - showNotification( - appContext.resources.getQuantityString( - R.plurals.export_partially_failed, - successfulExports, - successfulExports - ) + appContext.resources.getQuantityString( + R.plurals.export_partially_failed, + successfulExports, + successfulExports ) } + showNotification(message) } private fun showSuccessNotification(successfulExports: Int) { @@ -152,9 +152,7 @@ class FilesExportWork( NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD ) .setSmallIcon(R.drawable.notification_icon) - .setLargeIcon(BitmapFactory.decodeResource(appContext.resources, R.drawable.notification_icon)) - .setSubText(user.accountName) - .setContentText(message) + .setContentTitle(message) .setAutoCancel(true) viewThemeUtils.androidx.themeNotificationCompatBuilder(appContext, notificationBuilder) @@ -166,7 +164,8 @@ class FilesExportWork( appContext, notificationId, actionIntent, - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE + PendingIntent.FLAG_CANCEL_CURRENT or + PendingIntent.FLAG_IMMUTABLE ) notificationBuilder.addAction( NotificationCompat.Action( @@ -176,7 +175,8 @@ class FilesExportWork( ) ) - val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val notificationManager = appContext + .getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.notify(notificationId, notificationBuilder.build()) } diff --git a/app/src/main/java/com/nextcloud/ui/ImageDetailFragment.kt b/app/src/main/java/com/nextcloud/ui/ImageDetailFragment.kt index e858c6fa97d9..590e9e464fa7 100644 --- a/app/src/main/java/com/nextcloud/ui/ImageDetailFragment.kt +++ b/app/src/main/java/com/nextcloud/ui/ImageDetailFragment.kt @@ -30,6 +30,7 @@ import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.VisibleForTesting import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import com.nextcloud.android.common.ui.theme.utils.ColorRole @@ -260,6 +261,11 @@ class ImageDetailFragment : Fragment(), Injectable { binding.imageLocationMapCopyright.text = binding.imageLocationMap.tileProvider.tileSource.copyrightNotice } + @VisibleForTesting + fun hideMap() { + binding.imageLocationMap.visibility = View.GONE + } + @SuppressLint("SimpleDateFormat") private fun gatherMetadata() { val fileSize = DisplayUtils.bytesToHumanReadable(file.fileLength) diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/AccountRemovalConfirmationDialog.java b/app/src/main/java/com/owncloud/android/ui/dialog/AccountRemovalConfirmationDialog.java index f2863b19755e..a3a7e1391281 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/AccountRemovalConfirmationDialog.java +++ b/app/src/main/java/com/owncloud/android/ui/dialog/AccountRemovalConfirmationDialog.java @@ -25,6 +25,7 @@ import android.app.Dialog; import android.os.Bundle; +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; @@ -59,7 +60,11 @@ public static AccountRemovalConfirmationDialog newInstance(User user) { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - user = getArguments().getParcelable(KEY_USER); + + Bundle arguments = getArguments(); + if (arguments != null) { + user = arguments.getParcelable(KEY_USER); + } } @Override @@ -67,9 +72,18 @@ public void onStart() { super.onStart(); AlertDialog alertDialog = (AlertDialog) getDialog(); - - viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_POSITIVE), - alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL)); + if (alertDialog != null) { + + MaterialButton positiveButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); + if (positiveButton != null) { + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton); + } + + MaterialButton negativeButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE); + if (negativeButton != null) { + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton); + } + } } @NonNull @@ -82,7 +96,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { .setPositiveButton(R.string.common_ok, (dialogInterface, i) -> backgroundJobManager.startAccountRemovalJob(user.getAccountName(), false)) - .setNeutralButton(R.string.common_cancel, null); + .setNegativeButton(R.string.common_cancel, null); viewThemeUtils.dialog.colorMaterialAlertDialogBackground(requireActivity(), builder); 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 index b95a357e0fad..b62576fd822c 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseRichDocumentsTemplateDialogFragment.java @@ -35,6 +35,7 @@ import android.view.View; import android.widget.Button; +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; @@ -99,7 +100,7 @@ public class ChooseRichDocumentsTemplateDialogFragment extends DialogFragment im private RichDocumentsTemplateAdapter adapter; private OCFile parentFolder; private OwnCloudClient client; - private Button positiveButton; + private MaterialButton positiveButton; private DialogFragment waitDialog; public enum Type { @@ -126,11 +127,18 @@ public void onStart() { AlertDialog alertDialog = (AlertDialog) getDialog(); - positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); - viewThemeUtils.platform.colorTextButtons(positiveButton, - alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL)); - positiveButton.setOnClickListener(this); - positiveButton.setEnabled(false); + 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(); } @@ -205,12 +213,14 @@ public void afterTextChanged(Editable s) { } }); + int titleTextId = getTitle(type); + // Build the dialog MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity); builder.setView(view) .setPositiveButton(R.string.create, null) - .setNeutralButton(R.string.common_cancel, null) - .setTitle(getTitle(type)); + .setNegativeButton(R.string.common_cancel, null) + .setTitle(titleTextId); viewThemeUtils.dialog.colorMaterialAlertDialogBackground(activity, builder); diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt index a0e9aa514f28..b56513207ae2 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/ChooseTemplateDialogFragment.kt @@ -32,10 +32,10 @@ import android.os.Bundle import android.text.Editable import android.text.TextWatcher import android.view.View -import android.widget.Button import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import androidx.recyclerview.widget.GridLayoutManager +import com.google.android.material.button.MaterialButton import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.nextcloud.android.lib.resources.directediting.DirectEditingCreateFileRemoteOperation import com.nextcloud.android.lib.resources.directediting.DirectEditingObtainListOfTemplatesRemoteOperation @@ -90,7 +90,7 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem private var adapter: TemplateAdapter? = null private var parentFolder: OCFile? = null private var title: String? = null - private var positiveButton: Button? = null + private var positiveButton: MaterialButton? = null private var creator: Creator? = null enum class Type { @@ -103,17 +103,18 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem override fun onStart() { super.onStart() val alertDialog = dialog as AlertDialog - val button = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) - viewThemeUtils.platform.colorTextButtons( - button, - alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL) - ) - button.setOnClickListener(this) - button.isEnabled = false - button.isClickable = false + val positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton) + + val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as MaterialButton + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton) - positiveButton = button + positiveButton.setOnClickListener(this) + positiveButton.isEnabled = false + positiveButton.isClickable = false + + this.positiveButton = positiveButton checkEnablingCreateButton() } @@ -128,6 +129,7 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem parentFolder = arguments.getParcelable(ARG_PARENT_FOLDER) creator = arguments.getParcelable(ARG_CREATOR) + title = arguments.getString(ARG_HEADLINE, getString(R.string.select_template)) title = when (savedInstanceState) { null -> arguments.getString(ARG_HEADLINE) @@ -175,7 +177,7 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem val builder = MaterialAlertDialogBuilder(activity) builder.setView(view) .setPositiveButton(R.string.create, null) - .setNeutralButton(R.string.common_cancel, null) + .setNegativeButton(R.string.common_cancel, null) .setTitle(title) viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.list.context, builder) @@ -208,8 +210,8 @@ class ChooseTemplateDialogFragment : DialogFragment(), View.OnClickListener, Tem } fun setTemplateList(templateList: TemplateList?) { - adapter!!.setTemplateList(templateList) - adapter!!.notifyDataSetChanged() + adapter?.setTemplateList(templateList) + adapter?.notifyDataSetChanged() } override fun onClick(template: Template) { diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ConflictsResolveDialog.java b/app/src/main/java/com/owncloud/android/ui/dialog/ConflictsResolveDialog.java index a7333ffcab73..088e6f1aa127 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ConflictsResolveDialog.java +++ b/app/src/main/java/com/owncloud/android/ui/dialog/ConflictsResolveDialog.java @@ -29,6 +29,7 @@ import android.widget.Button; 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; @@ -70,7 +71,7 @@ public class ConflictsResolveDialog extends DialogFragment implements Injectable public OnConflictDecisionMadeListener listener; private User user; private final List asyncTasks = new ArrayList<>(); - private Button positiveButton; + private MaterialButton positiveButton; @Inject ViewThemeUtils viewThemeUtils; @Inject SyncedFolderProvider syncedFolderProvider; @@ -119,9 +120,11 @@ public void onStart() { return; } - positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); - viewThemeUtils.platform.colorTextButtons(positiveButton, - alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL)); + positiveButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); + MaterialButton negativeButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE); + + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton); + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton); positiveButton.setEnabled(false); } @@ -175,7 +178,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { } }) - .setNeutralButton(R.string.common_cancel, (dialog, which) -> { + .setNegativeButton(R.string.common_cancel, (dialog, which) -> { if (listener != null) { listener.conflictDecisionMade(Decision.CANCEL); } @@ -275,4 +278,5 @@ public void onStop() { asyncTasks.clear(); } + } diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.java b/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.java index 3703822aabed..c9a6d033767b 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.java @@ -31,6 +31,7 @@ import android.widget.Button; import android.widget.TextView; +import com.google.android.material.button.MaterialButton; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.common.collect.Sets; import com.nextcloud.client.di.Injectable; @@ -71,7 +72,7 @@ public class CreateFolderDialogFragment private OCFile mParentFolder; - private Button positiveButton; + private MaterialButton positiveButton; private EditBoxDialogBinding binding; @@ -101,13 +102,11 @@ public void onStart() { private void bindButton() { Dialog dialog = getDialog(); - if (dialog instanceof AlertDialog) { - AlertDialog alertDialog = (AlertDialog) dialog; - - positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); - - viewThemeUtils.platform.colorTextButtons(positiveButton, - alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL)); + if (dialog instanceof AlertDialog alertDialog) { + positiveButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); + MaterialButton negativeButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE); + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton); + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton); } } @@ -186,17 +185,25 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { }); // Build the dialog - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity()); - builder.setView(view) - .setPositiveButton(R.string.folder_confirm_create, this) - .setNeutralButton(R.string.common_cancel, this) - .setTitle(R.string.uploader_info_dirname); + MaterialAlertDialogBuilder builder = buildMaterialAlertDialog(view); viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.userInputContainer.getContext(), builder); return builder.create(); } + private MaterialAlertDialogBuilder buildMaterialAlertDialog(View view) { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity()); + + builder + .setView(view) + .setPositiveButton(R.string.folder_confirm_create, this) + .setNegativeButton(R.string.common_cancel, this) + .setTitle(R.string.uploader_info_dirname); + + return builder; + } + @Override public void onClick(DialogInterface dialog, int which) { if (which == AlertDialog.BUTTON_POSITIVE) { diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/ExpirationDatePickerDialogFragment.java b/app/src/main/java/com/owncloud/android/ui/dialog/ExpirationDatePickerDialogFragment.java index f1c16c6b49cf..5d97c7c1c5a9 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/ExpirationDatePickerDialogFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/dialog/ExpirationDatePickerDialogFragment.java @@ -30,6 +30,7 @@ import android.text.format.DateUtils; import android.widget.DatePicker; +import com.google.android.material.button.MaterialButton; import com.nextcloud.client.di.Injectable; import com.owncloud.android.R; import com.owncloud.android.utils.theme.ViewThemeUtils; @@ -81,12 +82,24 @@ public void setOnExpiryDateListener(OnExpiryDateListener onExpiryDateListener) { public void onStart() { super.onStart(); final Dialog currentDialog = getDialog(); + if (currentDialog != null) { final DatePickerDialog dialog = (DatePickerDialog) currentDialog; - viewThemeUtils.platform.colorTextButtons(dialog.getButton(DatePickerDialog.BUTTON_NEUTRAL), - dialog.getButton(DatePickerDialog.BUTTON_NEGATIVE), - dialog.getButton(DatePickerDialog.BUTTON_POSITIVE)); + MaterialButton positiveButton = (MaterialButton) dialog.getButton(DatePickerDialog.BUTTON_POSITIVE); + if (positiveButton != null) { + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton); + } + + MaterialButton negativeButton = (MaterialButton) dialog.getButton(DatePickerDialog.BUTTON_NEGATIVE); + if (negativeButton != null) { + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton); + } + + MaterialButton neutralButton = (MaterialButton) dialog.getButton(DatePickerDialog.BUTTON_NEUTRAL); + if (neutralButton != null) { + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(neutralButton); + } } } @@ -118,7 +131,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { //show unset button only when date is already selected if (chosenDateInMillis > 0) { dialog.setButton( - Dialog.BUTTON_NEUTRAL, + Dialog.BUTTON_NEGATIVE, getText(R.string.share_via_link_unset_password), (dialog1, which) -> { if (onExpiryDateListener != null) { diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/NoteDialogFragment.java b/app/src/main/java/com/owncloud/android/ui/dialog/NoteDialogFragment.java deleted file mode 100644 index 86ce2d6471fe..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/dialog/NoteDialogFragment.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Tobias Kaminsky - * Copyright (C) 2018 Tobias Kaminsky - * Copyright (C) 2018 Nextcloud GmbH. - * - * 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.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.NoteDialogBinding; -import com.owncloud.android.lib.resources.shares.OCShare; -import com.owncloud.android.ui.activity.ComponentsGetter; -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.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.DialogFragment; - -/** - * Dialog to input a multiline note for a share - */ -public class NoteDialogFragment extends DialogFragment implements DialogInterface.OnClickListener, Injectable { - - private static final String ARG_SHARE = "SHARE"; - - @Inject ViewThemeUtils viewThemeUtils; - @Inject KeyboardUtils keyboardUtils; - - private OCShare share; - private NoteDialogBinding binding; - - public static NoteDialogFragment newInstance(OCShare share) { - NoteDialogFragment frag = new NoteDialogFragment(); - - Bundle args = new Bundle(); - args.putParcelable(ARG_SHARE, share); - frag.setArguments(args); - - return frag; - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (getArguments() == null) { - throw new IllegalArgumentException("Arguments may not be null"); - } - share = getArguments().getParcelable(ARG_SHARE); - } - - @Override - public void onStart() { - super.onStart(); - - AlertDialog alertDialog = (AlertDialog) getDialog(); - - viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_POSITIVE), - alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL)); - } - - @Override - public void onResume() { - super.onResume(); - keyboardUtils.showKeyboardForEditText(requireDialog().getWindow(), binding.noteText); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - // Inflate the layout for the dialog - LayoutInflater inflater = requireActivity().getLayoutInflater(); - binding = NoteDialogBinding.inflate(inflater, null, false); - View view = binding.getRoot(); - - // Setup layout - binding.noteText.setText(share.getNote()); - viewThemeUtils.material.colorTextInputLayout(binding.noteContainer); - - // Build the dialog - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(binding.noteContainer.getContext()); - builder.setView(view) - .setPositiveButton(R.string.note_confirm, this) - .setNeutralButton(R.string.common_cancel, this) - .setTitle(R.string.send_note); - - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.noteContainer.getContext(), builder); - - return builder.create(); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - if (which == AlertDialog.BUTTON_POSITIVE) { - ComponentsGetter componentsGetter = (ComponentsGetter) getActivity(); - - if (componentsGetter != null) { - String note = ""; - - if (binding.noteText.getText() != null) { - note = binding.noteText.getText().toString().trim(); - } - - componentsGetter.getFileOperationsHelper().updateNoteToShare(share, note); - } else { - DisplayUtils.showSnackMessage(requireActivity(), R.string.note_could_not_sent); - } - } - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/RemoveFilesDialogFragment.java b/app/src/main/java/com/owncloud/android/ui/dialog/RemoveFilesDialogFragment.java index 64b832b34d51..087ff25035f8 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/RemoveFilesDialogFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/dialog/RemoveFilesDialogFragment.java @@ -23,6 +23,7 @@ import android.os.Bundle; import android.view.ActionMode; +import com.google.android.material.button.MaterialButton; import com.nextcloud.client.di.Injectable; import com.owncloud.android.R; import com.owncloud.android.datamodel.OCFile; @@ -96,15 +97,20 @@ public static RemoveFilesDialogFragment newInstance(ArrayList files) { R.string.confirmation_remove_files_alert; } - int localRemoveButton = (containsFolder || containsDown) ? R.string.confirmation_remove_local : -1; - args.putInt(ARG_MESSAGE_RESOURCE_ID, messageStringId); if (files.size() == SINGLE_SELECTION) { - args.putStringArray(ARG_MESSAGE_ARGUMENTS, new String[]{files.get(0).getFileName()}); + args.putStringArray(ARG_MESSAGE_ARGUMENTS, new String[] { files.get(0).getFileName() } ); } + args.putInt(ARG_POSITIVE_BTN_RES, R.string.file_delete); - args.putInt(ARG_NEUTRAL_BTN_RES, R.string.file_keep); - args.putInt(ARG_NEGATIVE_BTN_RES, localRemoveButton); + + if (containsFolder || containsDown) { + args.putInt(ARG_NEGATIVE_BTN_RES, R.string.confirmation_remove_local); + args.putInt(ARG_NEUTRAL_BTN_RES, R.string.file_keep); + } else { + args.putInt(ARG_NEGATIVE_BTN_RES, R.string.file_keep); + } + args.putParcelableArrayList(ARG_TARGET_FILES, files); frag.setArguments(args); @@ -131,9 +137,16 @@ public void 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)); + MaterialButton positiveButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton); + + MaterialButton negativeButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE); + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton); + + MaterialButton neutralButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL); + if (neutralButton != null) { + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(neutralButton); + } } } @@ -141,10 +154,14 @@ public void onStart() { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Dialog dialog = super.onCreateDialog(savedInstanceState); - mTargetFiles = getArguments().getParcelableArrayList(ARG_TARGET_FILES); + Bundle arguments = getArguments(); - setOnConfirmationListener(this); + if (arguments == null) { + return dialog; + } + mTargetFiles = arguments.getParcelableArrayList(ARG_TARGET_FILES); + setOnConfirmationListener(this); return dialog; } @@ -154,9 +171,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { */ @Override public void onConfirmation(String callerTag) { - ComponentsGetter cg = (ComponentsGetter) getActivity(); - cg.getFileOperationsHelper().removeFiles(mTargetFiles, false, false); - finishActionMode(); + removeFiles(false); } /** @@ -164,8 +179,14 @@ public void onConfirmation(String callerTag) { */ @Override public void onCancel(String callerTag) { + removeFiles(true); + } + + private void removeFiles(boolean onlyLocalCopy) { ComponentsGetter cg = (ComponentsGetter) getActivity(); - cg.getFileOperationsHelper().removeFiles(mTargetFiles, true, false); + if (cg != null) { + cg.getFileOperationsHelper().removeFiles(mTargetFiles, onlyLocalCopy, false); + } finishActionMode(); } diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/RenamePublicShareDialogFragment.java b/app/src/main/java/com/owncloud/android/ui/dialog/RenamePublicShareDialogFragment.java index e043a8975acc..7973b93fafe0 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/RenamePublicShareDialogFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/dialog/RenamePublicShareDialogFragment.java @@ -28,6 +28,7 @@ import android.view.LayoutInflater; import android.view.View; +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; @@ -52,8 +53,6 @@ public class RenamePublicShareDialogFragment private static final String ARG_PUBLIC_SHARE = "PUBLIC_SHARE"; - public static final String RENAME_PUBLIC_SHARE_FRAGMENT = "RENAME_PUBLIC_SHARE_FRAGMENT"; - @Inject ViewThemeUtils viewThemeUtils; @Inject KeyboardUtils keyboardUtils; @@ -75,8 +74,10 @@ public void onStart() { AlertDialog alertDialog = (AlertDialog) getDialog(); if (alertDialog != null) { - viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_POSITIVE), - alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL)); + MaterialButton positiveButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); + MaterialButton negativeButton = (MaterialButton) alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE); + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton); + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton); } } @@ -104,7 +105,7 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(view.getContext()); builder.setView(view) .setPositiveButton(R.string.file_rename, this) - .setNeutralButton(R.string.common_cancel, this) + .setNegativeButton(R.string.common_cancel, this) .setTitle(R.string.public_share_name); viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.userInput.getContext(), builder); diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SslValidatorDialog.java b/app/src/main/java/com/owncloud/android/ui/dialog/SslValidatorDialog.java deleted file mode 100644 index 2373681946bd..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/dialog/SslValidatorDialog.java +++ /dev/null @@ -1,329 +0,0 @@ -/** - * ownCloud Android client application - * - * @author David A. Velasco - * 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.content.Context; -import android.os.Bundle; -import android.view.View; -import android.view.Window; -import android.widget.Button; - -import com.owncloud.android.R; -import com.owncloud.android.databinding.SslValidatorLayoutBinding; -import com.owncloud.android.lib.common.network.CertificateCombinedException; -import com.owncloud.android.lib.common.network.NetworkUtils; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.utils.Log_OC; - -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -import javax.security.auth.x500.X500Principal; - -/** - * Dialog to request the user about a certificate that could not be validated with the certificates store in the system. - */ -public class SslValidatorDialog extends Dialog { - - private final static String TAG = SslValidatorDialog.class.getSimpleName(); - - private OnSslValidatorListener mListener; - private CertificateCombinedException mException; - private SslValidatorLayoutBinding binding; - - - /** - * Creates a new SslValidatorDialog to ask the user if an untrusted certificate from a server should - * be trusted. - * - * @param context Android context where the dialog will live. - * @param result Result of a failed remote operation. - * @param listener Object to notice when the server certificate was added to the local certificates store. - * @return A new SslValidatorDialog instance. NULL if the operation can not be recovered - * by setting the certificate as reliable. - */ - public static SslValidatorDialog newInstance(Context context, RemoteOperationResult result, OnSslValidatorListener listener) { - if (result != null && result.isSslRecoverableException()) { - return new SslValidatorDialog(context, listener); - } else { - return null; - } - } - - /** - * Private constructor. - * - * Instances have to be created through static {@link SslValidatorDialog#newInstance}. - * - * @param context Android context where the dialog will live - * @param listener Object to notice when the server certificate was added to the local certificates store. - */ - private SslValidatorDialog(Context context, OnSslValidatorListener listener) { - super(context); - mListener = listener; - } - - /** - * {@inheritDoc} - */ - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - requestWindowFeature(Window.FEATURE_NO_TITLE); - binding = SslValidatorLayoutBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - binding.ok.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - try { - saveServerCert(); - dismiss(); - if (mListener != null) { - mListener.onSavedCertificate(); - } else { - Log_OC.d(TAG, "Nobody there to notify the certificate was saved"); - } - - } catch (GeneralSecurityException | IOException e) { - dismiss(); - if (mListener != null) { - mListener.onFailedSavingCertificate(); - } - Log_OC.e(TAG, "Server certificate could not be saved in the known servers trust store ", e); - - } - } - }); - - binding.cancel.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - cancel(); - } - }); - - binding.detailsBtn.setOnClickListener( - new View.OnClickListener() { - @Override - public void onClick(View v) { - View detailsScroll = findViewById(R.id.details_scroll); - if (detailsScroll.getVisibility() == View.VISIBLE) { - detailsScroll.setVisibility(View.GONE); - ((Button) v).setText(R.string.ssl_validator_btn_details_see); - } else { - detailsScroll.setVisibility(View.VISIBLE); - ((Button) v).setText(R.string.ssl_validator_btn_details_hide); - } - } - }); - } - - - public void updateResult(RemoteOperationResult result) { - if (result.isSslRecoverableException()) { - mException = (CertificateCombinedException) result.getException(); - - /// clean - binding.reasonCertNotTrusted.setVisibility(View.GONE); - binding.reasonCertExpired.setVisibility(View.GONE); - binding.reasonCertNotYetValid.setVisibility(View.GONE); - binding.reasonHostnameNotVerified.setVisibility(View.GONE); - binding.detailsScroll.setVisibility(View.GONE); - - /// refresh - if (mException.getCertPathValidatorException() != null) { - binding.reasonCertNotTrusted.setVisibility(View.VISIBLE); - } - - if (mException.getCertificateExpiredException() != null) { - binding.reasonCertExpired.setVisibility(View.VISIBLE); - } - - if (mException.getCertificateNotYetValidException() != null) { - binding.reasonCertNotYetValid.setVisibility(View.VISIBLE); - } - - if (mException.getSslPeerUnverifiedException() != null ) { - binding.reasonHostnameNotVerified.setVisibility(View.VISIBLE); - } - - showCertificateData(mException.getServerCertificate()); - } - } - - private void showCertificateData(X509Certificate cert) { - - if (cert != null) { - showSubject(cert.getSubjectX500Principal()); - showIssuer(cert.getIssuerX500Principal()); - showValidity(cert.getNotBefore(), cert.getNotAfter()); - showSignature(cert); - - } else { - // this should not happen, TODO - Log_OC.d("certNull", "This should not happen"); - } - } - - private void showSignature(X509Certificate cert) { - binding.valueSignature.setText(getHex(cert.getSignature())); - binding.valueSignatureAlgorithm.setText(cert.getSigAlgName()); - } - - public String getHex(final byte [] raw) { - if (raw == null) { - return null; - } - final StringBuilder hex = new StringBuilder(2 * raw.length); - for (final byte b : raw) { - final int hiVal = (b & 0xF0) >> 4; - final int loVal = b & 0x0F; - hex.append((char) ('0' + (hiVal + (hiVal / 10 * 7)))); - hex.append((char) ('0' + (loVal + (loVal / 10 * 7)))); - } - return hex.toString(); - } - - @SuppressWarnings("deprecation") - private void showValidity(Date notBefore, Date notAfter) { - binding.valueValidityFrom.setText(notBefore.toLocaleString()); - binding.valueValidityTo.setText(notAfter.toLocaleString()); - } - - private void showSubject(X500Principal subject) { - Map s = parsePrincipal(subject); - - if (s.get("CN") != null) { - binding.valueSubjectCN.setText(s.get("CN")); - binding.valueSubjectCN.setVisibility(View.VISIBLE); - } else { - binding.valueSubjectCN.setVisibility(View.GONE); - } - if (s.get("O") != null) { - binding.valueSubjectO.setText(s.get("O")); - binding.valueSubjectO.setVisibility(View.VISIBLE); - } else { - binding.valueSubjectO.setVisibility(View.GONE); - } - if (s.get("OU") != null) { - binding.valueSubjectOU.setText(s.get("OU")); - binding.valueSubjectOU.setVisibility(View.VISIBLE); - } else { - binding.valueSubjectOU.setVisibility(View.GONE); - } - if (s.get("C") != null) { - binding.valueSubjectC.setText(s.get("C")); - binding.valueSubjectC.setVisibility(View.VISIBLE); - } else { - binding.valueSubjectC.setVisibility(View.GONE); - } - if (s.get("ST") != null) { - binding.valueSubjectST.setText(s.get("ST")); - binding.valueSubjectST.setVisibility(View.VISIBLE); - } else { - binding.valueSubjectST.setVisibility(View.GONE); - } - if (s.get("L") != null) { - binding.valueSubjectL.setText(s.get("L")); - binding.valueSubjectL.setVisibility(View.VISIBLE); - } else { - binding.valueSubjectL.setVisibility(View.GONE); - } - } - - private void showIssuer(X500Principal issuer) { - Map s = parsePrincipal(issuer); - - if (s.get("CN") != null) { - binding.valueIssuerCN.setText(s.get("CN")); - binding.valueIssuerCN.setVisibility(View.VISIBLE); - } else { - binding.valueIssuerCN.setVisibility(View.GONE); - } - if (s.get("O") != null) { - binding.valueIssuerO.setText(s.get("O")); - binding.valueIssuerO.setVisibility(View.VISIBLE); - } else { - binding.valueIssuerO.setVisibility(View.GONE); - } - if (s.get("OU") != null) { - binding.valueIssuerOU.setText(s.get("OU")); - binding.valueIssuerOU.setVisibility(View.VISIBLE); - } else { - binding.valueIssuerOU.setVisibility(View.GONE); - } - if (s.get("C") != null) { - binding.valueIssuerC.setText(s.get("C")); - binding.valueIssuerC.setVisibility(View.VISIBLE); - } else { - binding.valueIssuerC.setVisibility(View.GONE); - } - if (s.get("ST") != null) { - binding.valueIssuerST.setText(s.get("ST")); - binding.valueIssuerST.setVisibility(View.VISIBLE); - } else { - binding.valueIssuerST.setVisibility(View.GONE); - } - if (s.get("L") != null) { - binding.valueIssuerL.setText(s.get("L")); - binding.valueIssuerL.setVisibility(View.VISIBLE); - } else { - binding.valueIssuerL.setVisibility(View.GONE); - } - } - - private Map parsePrincipal(X500Principal principal) { - Map result = new HashMap<>(); - String toParse = principal.getName(); - String[] pieces = toParse.split(","); - String[] tokens = {"CN", "O", "OU", "C", "ST", "L"}; - for (String piece : pieces) { - for (String token : tokens) { - if (piece.startsWith(token + "=")) { - result.put(token, piece.substring(token.length() + 1)); - } - } - } - return result; - } - - private void saveServerCert() throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { - if (mException.getServerCertificate() != null) { - // TODO make this asynchronously, it can take some time - NetworkUtils.addCertToKnownServersStore(mException.getServerCertificate(), getContext()); - } - } - - public interface OnSslValidatorListener { - public void onSavedCertificate(); - public void onFailedSavingCertificate(); - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/StoragePermissionDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/StoragePermissionDialogFragment.kt index c3cddfcc30a1..d2b179e66628 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/StoragePermissionDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/StoragePermissionDialogFragment.kt @@ -24,15 +24,14 @@ import android.app.Dialog import android.os.Build import android.os.Bundle import android.os.Parcelable -import android.view.View import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog import androidx.core.os.bundleOf 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.StoragePermissionDialogBinding import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.parcelize.Parcelize import javax.inject.Inject @@ -43,10 +42,7 @@ import javax.inject.Inject * Allows choosing "full access" (MANAGE_ALL_FILES) or "read-only media" (READ_EXTERNAL_STORAGE) */ @RequiresApi(Build.VERSION_CODES.R) -class StoragePermissionDialogFragment : - DialogFragment(), Injectable { - - private lateinit var binding: StoragePermissionDialogBinding +class StoragePermissionDialogFragment : DialogFragment(), Injectable { private var permissionRequired = false @@ -64,51 +60,48 @@ class StoragePermissionDialogFragment : super.onStart() dialog?.let { val alertDialog = it as AlertDialog - viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE)) - } - } - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - // Inflate the layout for the dialog - val inflater = requireActivity().layoutInflater - binding = StoragePermissionDialogBinding.inflate(inflater, null, false) + val positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) as MaterialButton + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(positiveButton) - val view: View = binding.root - val explanationResource = when { - permissionRequired -> R.string.file_management_permission_text - else -> R.string.file_management_permission_optional_text - } - binding.storagePermissionExplanation.text = getString(explanationResource, getString(R.string.app_name)) + val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) as MaterialButton + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(negativeButton) - // Setup layout - viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.btnFullAccess) - binding.btnFullAccess.setOnClickListener { - setResult(Result.FULL_ACCESS) - dismiss() - } - viewThemeUtils.platform.colorTextButtons(binding.btnReadOnly) - binding.btnReadOnly.setOnClickListener { - setResult(Result.MEDIA_READ_ONLY) - dismiss() + val neutralButton = alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL) as MaterialButton + viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(neutralButton) } + } - // Build the dialog - val titleResource = when { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val title = when { permissionRequired -> R.string.file_management_permission else -> R.string.file_management_permission_optional } + val explanationResource = when { + permissionRequired -> R.string.file_management_permission_text + else -> R.string.file_management_permission_optional_text + } + val message = getString(explanationResource, getString(R.string.app_name)) - val builder = MaterialAlertDialogBuilder(binding.btnReadOnly.context) - .setTitle(titleResource) - .setView(view) - .setNegativeButton(R.string.common_cancel) { _, _ -> + val dialogBuilder = MaterialAlertDialogBuilder(requireContext()) + .setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.storage_permission_full_access) { _, _ -> + setResult(Result.FULL_ACCESS) + dismiss() + } + .setNegativeButton(R.string.storage_permission_media_read_only) { _, _ -> + setResult(Result.MEDIA_READ_ONLY) + dismiss() + } + .setNeutralButton(R.string.common_cancel) { _, _ -> setResult(Result.CANCEL) dismiss() } - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.btnReadOnly.context, builder) + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(requireContext(), dialogBuilder) - return builder.create() + return dialogBuilder.create() } private fun setResult(result: Result) { diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java index 8991de8c5536..871a4624a13e 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailActivitiesFragment.java @@ -284,6 +284,10 @@ private void fetchAndSetData(int lastGiven) { }); return; } + + if (!isLoadingActivities) { + return; + } Thread t = new Thread(() -> { try { @@ -454,6 +458,11 @@ public void avatarGenerated(Drawable avatarDrawable, Object callContext) { public boolean shouldCallGeneratedCallback(String tag, Object callContext) { return false; } + + @VisibleForTesting + public void disableLoadingActivities() { + isLoadingActivities = false; + } private static class SubmitCommentTask extends AsyncTask { diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupFragment.java deleted file mode 100644 index aad71fe6246f..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupFragment.java +++ /dev/null @@ -1,691 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Mario Danic - * @author TSI-mc - * Copyright (C) 2017 Mario Danic - * Copyright (C) 2017 Nextcloud GmbH. - * Copyright (C) 2023 TSI-mc - * - * 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.fragment.contactsbackup; - -import android.Manifest; -import android.app.DatePickerDialog; -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CompoundButton; -import android.widget.DatePicker; -import android.widget.Toast; - -import com.nextcloud.client.account.User; -import com.nextcloud.client.di.Injectable; -import com.nextcloud.client.jobs.BackgroundJobManager; -import com.nextcloud.java.util.Optional; -import com.owncloud.android.R; -import com.owncloud.android.databinding.BackupFragmentBinding; -import com.owncloud.android.datamodel.ArbitraryDataProvider; -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.operations.RefreshFolderOperation; -import com.owncloud.android.ui.activity.ContactsPreferenceActivity; -import com.owncloud.android.ui.activity.SettingsActivity; -import com.owncloud.android.ui.fragment.FileFragment; -import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.MimeTypeUtil; -import com.owncloud.android.utils.PermissionUtil; -import com.owncloud.android.utils.theme.ThemeUtils; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.List; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.fragment.app.Fragment; -import third_parties.daveKoeller.AlphanumComparator; - -import static com.owncloud.android.ui.activity.ContactsPreferenceActivity.PREFERENCE_CONTACTS_AUTOMATIC_BACKUP; -import static com.owncloud.android.ui.activity.ContactsPreferenceActivity.PREFERENCE_CONTACTS_LAST_BACKUP; - -public class BackupFragment extends FileFragment implements DatePickerDialog.OnDateSetListener, Injectable { - public static final String TAG = BackupFragment.class.getSimpleName(); - private static final String ARG_SHOW_SIDEBAR = "SHOW_SIDEBAR"; - private static final String KEY_CALENDAR_PICKER_OPEN = "IS_CALENDAR_PICKER_OPEN"; - private static final String KEY_CALENDAR_DAY = "CALENDAR_DAY"; - private static final String KEY_CALENDAR_MONTH = "CALENDAR_MONTH"; - private static final String KEY_CALENDAR_YEAR = "CALENDAR_YEAR"; - - public static final String PREFERENCE_CONTACTS_BACKUP_ENABLED = "PREFERENCE_CONTACTS_BACKUP_ENABLED"; - public static final String PREFERENCE_CALENDAR_BACKUP_ENABLED = "PREFERENCE_CALENDAR_BACKUP_ENABLED"; - - - private BackupFragmentBinding binding; - - @Inject BackgroundJobManager backgroundJobManager; - @Inject ThemeUtils themeUtils; - - @Inject ArbitraryDataProvider arbitraryDataProvider; - @Inject ViewThemeUtils viewThemeUtils; - - private Date selectedDate; - private boolean calendarPickerOpen; - - private DatePickerDialog datePickerDialog; - - private CompoundButton.OnCheckedChangeListener dailyBackupCheckedChangeListener; - private CompoundButton.OnCheckedChangeListener contactsCheckedListener; - private CompoundButton.OnCheckedChangeListener calendarCheckedListener; - private User user; - private boolean showSidebar = true; - //flag to check if calendar backup should be shown and backup should be done or not - private boolean showCalendarBackup = true; - public static BackupFragment create(boolean showSidebar) { - BackupFragment fragment = new BackupFragment(); - Bundle bundle = new Bundle(); - bundle.putBoolean(ARG_SHOW_SIDEBAR, showSidebar); - fragment.setArguments(bundle); - return fragment; - } - - private boolean isCalendarBackupEnabled() { - return arbitraryDataProvider.getBooleanValue(user, PREFERENCE_CALENDAR_BACKUP_ENABLED); - } - - private void setCalendarBackupEnabled(final boolean enabled) { - arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(), PREFERENCE_CALENDAR_BACKUP_ENABLED, enabled); - } - - private boolean isContactsBackupEnabled() { - return arbitraryDataProvider.getBooleanValue(user, PREFERENCE_CONTACTS_BACKUP_ENABLED); - } - - private void setContactsBackupEnabled(final boolean enabled) { - arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(), PREFERENCE_CONTACTS_BACKUP_ENABLED, enabled); - } - - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - - // use grey as fallback for elements where custom theming is not available - if (themeUtils.themingEnabled(getContext())) { - getContext().getTheme().applyStyle(R.style.FallbackThemingTheme, true); - } - - binding = BackupFragmentBinding.inflate(inflater, container, false); - View view = binding.getRoot(); - - setHasOptionsMenu(true); - - if (getArguments() != null) { - showSidebar = getArguments().getBoolean(ARG_SHOW_SIDEBAR); - } - - showCalendarBackup = requireContext().getResources().getBoolean(R.bool.show_calendar_backup); - - final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity(); - user = contactsPreferenceActivity.getUser().orElseThrow(RuntimeException::new); - - ActionBar actionBar = contactsPreferenceActivity != null ? contactsPreferenceActivity.getSupportActionBar() : null; - - if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(true); - viewThemeUtils.files.themeActionBar(requireContext(), actionBar, - showCalendarBackup ? R.string.backup_title : R.string.contact_backup_title); - } - - - viewThemeUtils.androidx.colorSwitchCompat(binding.contacts); - viewThemeUtils.androidx.colorSwitchCompat(binding.calendar); - viewThemeUtils.androidx.colorSwitchCompat(binding.dailyBackup); - binding.dailyBackup.setChecked(arbitraryDataProvider.getBooleanValue(user, - PREFERENCE_CONTACTS_AUTOMATIC_BACKUP)); - - binding.contacts.setChecked(isContactsBackupEnabled() && checkContactBackupPermission()); - binding.calendar.setChecked(isCalendarBackupEnabled() && checkCalendarBackupPermission(getContext())); - - binding.calendar.setVisibility(showCalendarBackup ? View.VISIBLE : View.GONE); - - setupCheckListeners(); - - setBackupNowButtonVisibility(); - - binding.backupNow.setOnClickListener(v -> backupNow()); - - binding.contactsDatepicker.setOnClickListener(v -> openCleanDate()); - - // display last backup - Long lastBackupTimestamp = arbitraryDataProvider.getLongValue(user, PREFERENCE_CONTACTS_LAST_BACKUP); - - if (lastBackupTimestamp == -1) { - binding.lastBackupWithDate.setVisibility(View.GONE); - } else { - binding.lastBackupWithDate.setText( - String.format(getString(R.string.last_backup), - DisplayUtils.getRelativeTimestamp(contactsPreferenceActivity, lastBackupTimestamp))); - } - - if (savedInstanceState != null && savedInstanceState.getBoolean(KEY_CALENDAR_PICKER_OPEN, false)) { - if (savedInstanceState.getInt(KEY_CALENDAR_YEAR, -1) != -1 && - savedInstanceState.getInt(KEY_CALENDAR_MONTH, -1) != -1 && - savedInstanceState.getInt(KEY_CALENDAR_DAY, -1) != -1) { - selectedDate = new Date(savedInstanceState.getInt(KEY_CALENDAR_YEAR), - savedInstanceState.getInt(KEY_CALENDAR_MONTH), savedInstanceState.getInt(KEY_CALENDAR_DAY)); - } - calendarPickerOpen = true; - } - - viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.backupNow); - viewThemeUtils.material.colorMaterialButtonPrimaryOutlined(binding.contactsDatepicker); - - viewThemeUtils.platform.colorTextView(binding.dataToBackUpTitle); - viewThemeUtils.platform.colorTextView(binding.backupSettingsTitle); - - return view; - } - - private void setupCheckListeners() { - dailyBackupCheckedChangeListener = (buttonView, isChecked) -> { - if (checkAndAskForContactsReadPermission()) { - setAutomaticBackup(isChecked); - } - }; - binding.dailyBackup.setOnCheckedChangeListener(dailyBackupCheckedChangeListener); - - - contactsCheckedListener = (buttonView, isChecked) -> { - if (isChecked) { - if (checkAndAskForContactsReadPermission()) { - setContactsBackupEnabled(true); - } - } else { - setContactsBackupEnabled(false); - } - setBackupNowButtonVisibility(); - setAutomaticBackup(binding.dailyBackup.isChecked()); - }; - binding.contacts.setOnCheckedChangeListener(contactsCheckedListener); - - calendarCheckedListener = (buttonView, isChecked) -> { - if (isChecked) { - if (checkAndAskForCalendarReadPermission()) { - setCalendarBackupEnabled(true); - } - } else { - setCalendarBackupEnabled(false); - } - setBackupNowButtonVisibility(); - setAutomaticBackup(binding.dailyBackup.isChecked()); - }; - binding.calendar.setOnCheckedChangeListener(calendarCheckedListener); - } - - private void setBackupNowButtonVisibility() { - if (binding.contacts.isChecked() || binding.calendar.isChecked()) { - binding.backupNow.setVisibility(View.VISIBLE); - } else { - binding.backupNow.setVisibility(View.INVISIBLE); - } - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - } - - @Override - public void onResume() { - super.onResume(); - - if (calendarPickerOpen) { - if (selectedDate != null) { - openDate(selectedDate); - } else { - openDate(null); - } - } - - final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity(); - if (contactsPreferenceActivity != null) { - String backupFolderPath = getResources().getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR; - refreshBackupFolder(backupFolderPath, - contactsPreferenceActivity.getApplicationContext(), - contactsPreferenceActivity.getStorageManager()); - } - } - - private void refreshBackupFolder(final String backupFolderPath, - final Context context, - final FileDataStorageManager storageManager) { - AsyncTask task = new AsyncTask() { - @Override - protected Boolean doInBackground(String... path) { - OCFile folder = storageManager.getFileByPath(path[0]); - - if (folder != null) { - RefreshFolderOperation operation = new RefreshFolderOperation(folder, System.currentTimeMillis(), - false, false, storageManager, user, context); - - RemoteOperationResult result = operation.execute(user, context); - return result.isSuccess(); - } else { - return Boolean.FALSE; - } - } - - @Override - protected void onPostExecute(Boolean result) { - if (result && binding != null) { - OCFile backupFolder = storageManager.getFileByPath(backupFolderPath); - - List backupFiles = storageManager - .getFolderContent(backupFolder, false); - - Collections.sort(backupFiles, new AlphanumComparator<>()); - - if (backupFiles == null || backupFiles.isEmpty()) { - binding.contactsDatepicker.setVisibility(View.INVISIBLE); - } else { - binding.contactsDatepicker.setVisibility(View.VISIBLE); - } - } - } - }; - - task.execute(backupFolderPath); - } - - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity(); - - boolean retval; - switch (item.getItemId()) { - case android.R.id.home: - if (showSidebar) { - if (contactsPreferenceActivity.isDrawerOpen()) { - contactsPreferenceActivity.closeDrawer(); - } else { - contactsPreferenceActivity.openDrawer(); - } - } else if (getActivity() != null) { - getActivity().finish(); - } else { - Intent settingsIntent = new Intent(getContext(), SettingsActivity.class); - settingsIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(settingsIntent); - } - retval = true; - break; - - default: - retval = super.onOptionsItemSelected(item); - break; - } - return retval; - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - - if (requestCode == PermissionUtil.PERMISSIONS_READ_CONTACTS_AUTOMATIC) { - for (int index = 0; index < permissions.length; index++) { - if (Manifest.permission.READ_CONTACTS.equalsIgnoreCase(permissions[index])) { - if (grantResults[index] >= 0) { - // if approved, exit for loop - setContactsBackupEnabled(true); - break; - } - - // if not accepted, disable again - binding.contacts.setOnCheckedChangeListener(null); - binding.contacts.setChecked(false); - binding.contacts.setOnCheckedChangeListener(contactsCheckedListener); - } - } - } - - if (requestCode == PermissionUtil.PERMISSIONS_READ_CALENDAR_AUTOMATIC) { - boolean readGranted = false; - boolean writeGranted = false; - for (int index = 0; index < permissions.length; index++) { - if (Manifest.permission.WRITE_CALENDAR.equalsIgnoreCase(permissions[index]) && grantResults[index] >= 0) { - writeGranted = true; - } else if (Manifest.permission.READ_CALENDAR.equalsIgnoreCase(permissions[index]) && grantResults[index] >= 0) { - readGranted = true; - } - } - if (!readGranted || !writeGranted) { - // if not accepted, disable again - binding.calendar.setOnCheckedChangeListener(null); - binding.calendar.setChecked(false); - binding.calendar.setOnCheckedChangeListener(calendarCheckedListener); - } else { - setCalendarBackupEnabled(true); - } - } - - setBackupNowButtonVisibility(); - setAutomaticBackup(binding.dailyBackup.isChecked()); - } - - public void backupNow() { - if (isContactsBackupEnabled() && checkContactBackupPermission()) { - startContactsBackupJob(); - } - - if (showCalendarBackup && isCalendarBackupEnabled() && checkCalendarBackupPermission(requireContext())) { - startCalendarBackupJob(); - } - - DisplayUtils.showSnackMessage(requireView().findViewById(R.id.contacts_linear_layout), - R.string.contacts_preferences_backup_scheduled); - } - - private void startContactsBackupJob() { - ContactsPreferenceActivity activity = (ContactsPreferenceActivity) getActivity(); - if (activity != null) { - Optional optionalUser = activity.getUser(); - if (optionalUser.isPresent()) { - backgroundJobManager.startImmediateContactsBackup(optionalUser.get()); - } - } - } - - private void startCalendarBackupJob() { - ContactsPreferenceActivity activity = (ContactsPreferenceActivity) getActivity(); - if (activity != null) { - Optional optionalUser = activity.getUser(); - if (optionalUser.isPresent()) { - backgroundJobManager.startImmediateCalendarBackup(optionalUser.get()); - } - } - } - - private void setAutomaticBackup(final boolean enabled) { - - final ContactsPreferenceActivity activity = (ContactsPreferenceActivity) getActivity(); - if (activity == null) { - return; - } - Optional optionalUser = activity.getUser(); - if (!optionalUser.isPresent()) { - return; - } - User user = optionalUser.get(); - if (enabled) { - if (isContactsBackupEnabled()) { - Log_OC.d(TAG, "Scheduling contacts backup job"); - backgroundJobManager.schedulePeriodicContactsBackup(user); - } else { - Log_OC.d(TAG, "Cancelling contacts backup job"); - backgroundJobManager.cancelPeriodicContactsBackup(user); - } - if (isCalendarBackupEnabled()) { - Log_OC.d(TAG, "Scheduling calendar backup job"); - backgroundJobManager.schedulePeriodicCalendarBackup(user); - } else { - Log_OC.d(TAG, "Cancelling calendar backup job"); - backgroundJobManager.cancelPeriodicCalendarBackup(user); - } - } else { - Log_OC.d(TAG, "Cancelling all backup jobs"); - backgroundJobManager.cancelPeriodicContactsBackup(user); - backgroundJobManager.cancelPeriodicCalendarBackup(user); - } - - arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(), - PREFERENCE_CONTACTS_AUTOMATIC_BACKUP, - String.valueOf(enabled)); - } - - private boolean checkAndAskForContactsReadPermission() { - final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity(); - - // check permissions - if (PermissionUtil.checkSelfPermission(contactsPreferenceActivity, Manifest.permission.READ_CONTACTS)) { - return true; - } else { - // No explanation needed, request the permission. - requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, - PermissionUtil.PERMISSIONS_READ_CONTACTS_AUTOMATIC); - return false; - } - } - - private boolean checkAndAskForCalendarReadPermission() { - final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity(); - - // check permissions - if (checkCalendarBackupPermission(contactsPreferenceActivity)) { - return true; - } else { - // No explanation needed, request the permission. - requestPermissions(new String[]{Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR}, - PermissionUtil.PERMISSIONS_READ_CALENDAR_AUTOMATIC); - return false; - } - } - - private boolean checkCalendarBackupPermission(final Context context) { - return PermissionUtil.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) && PermissionUtil.checkSelfPermission(context, Manifest.permission.WRITE_CALENDAR); - } - - private boolean checkContactBackupPermission() { - return PermissionUtil.checkSelfPermission(getContext(), Manifest.permission.READ_CONTACTS); - } - - public void openCleanDate() { - if (checkAndAskForCalendarReadPermission() && checkAndAskForContactsReadPermission()) { - openDate(null); - } - } - - public void openDate(@Nullable Date savedDate) { - final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity(); - - if (contactsPreferenceActivity == null) { - Toast.makeText(getContext(), getString(R.string.error_choosing_date), Toast.LENGTH_LONG).show(); - return; - } - - String contactsBackupFolderString = - getResources().getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR; - String calendarBackupFolderString = - getResources().getString(R.string.calendar_backup_folder) + OCFile.PATH_SEPARATOR; - - FileDataStorageManager storageManager = contactsPreferenceActivity.getStorageManager(); - - OCFile contactsBackupFolder = storageManager.getFileByDecryptedRemotePath(contactsBackupFolderString); - OCFile calendarBackupFolder = storageManager.getFileByDecryptedRemotePath(calendarBackupFolderString); - - List backupFiles = storageManager.getFolderContent(contactsBackupFolder, false); - backupFiles.addAll(storageManager.getFolderContent(calendarBackupFolder, false)); - - Collections.sort(backupFiles, (o1, o2) -> { - return Long.compare(o1.getModificationTimestamp(), o2.getModificationTimestamp()); - }); - - Calendar cal = Calendar.getInstance(); - int year; - int month; - int day; - - if (savedDate == null) { - year = cal.get(Calendar.YEAR); - month = cal.get(Calendar.MONTH) + 1; - day = cal.get(Calendar.DAY_OF_MONTH); - } else { - year = savedDate.getYear(); - month = savedDate.getMonth(); - day = savedDate.getDay(); - } - - if (backupFiles.size() > 0 && backupFiles.get(backupFiles.size() - 1) != null) { - datePickerDialog = new DatePickerDialog(contactsPreferenceActivity, this, year, month, day); - datePickerDialog.getDatePicker().setMaxDate(backupFiles.get(backupFiles.size() - 1) - .getModificationTimestamp()); - datePickerDialog.getDatePicker().setMinDate(backupFiles.get(0).getModificationTimestamp()); - - datePickerDialog.setOnDismissListener(dialog -> selectedDate = null); - - datePickerDialog.setTitle(""); - datePickerDialog.show(); - - viewThemeUtils.platform.colorTextButtons(datePickerDialog.getButton(DatePickerDialog.BUTTON_NEGATIVE), - datePickerDialog.getButton(DatePickerDialog.BUTTON_POSITIVE)); - - // set background to transparent - datePickerDialog.getButton(DatePickerDialog.BUTTON_NEGATIVE).setBackgroundColor(0x00000000); - datePickerDialog.getButton(DatePickerDialog.BUTTON_POSITIVE).setBackgroundColor(0x00000000); - } else { - DisplayUtils.showSnackMessage(getView().findViewById(R.id.contacts_linear_layout), - R.string.contacts_preferences_something_strange_happened); - } - } - - @Override - public void onDestroyView() { - binding = null; - super.onDestroyView(); - } - - @Override - public void onStop() { - super.onStop(); - if (datePickerDialog != null) { - datePickerDialog.dismiss(); - } - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - if (datePickerDialog != null) { - outState.putBoolean(KEY_CALENDAR_PICKER_OPEN, datePickerDialog.isShowing()); - - if (datePickerDialog.isShowing()) { - outState.putInt(KEY_CALENDAR_DAY, datePickerDialog.getDatePicker().getDayOfMonth()); - outState.putInt(KEY_CALENDAR_MONTH, datePickerDialog.getDatePicker().getMonth()); - outState.putInt(KEY_CALENDAR_YEAR, datePickerDialog.getDatePicker().getYear()); - } - } - } - - @Override - public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) { - final ContactsPreferenceActivity contactsPreferenceActivity = (ContactsPreferenceActivity) getActivity(); - - if (contactsPreferenceActivity == null) { - Toast.makeText(getContext(), getString(R.string.error_choosing_date), Toast.LENGTH_LONG).show(); - return; - } - - selectedDate = new Date(year, month, dayOfMonth); - - String contactsBackupFolderString = - getResources().getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR; - String calendarBackupFolderString = - getResources().getString(R.string.calendar_backup_folder) + OCFile.PATH_SEPARATOR; - - FileDataStorageManager storageManager = contactsPreferenceActivity.getStorageManager(); - - OCFile contactsBackupFolder = storageManager.getFileByDecryptedRemotePath(contactsBackupFolderString); - OCFile calendarBackupFolder = storageManager.getFileByDecryptedRemotePath(calendarBackupFolderString); - - List backupFiles = storageManager.getFolderContent(contactsBackupFolder, false); - backupFiles.addAll(storageManager.getFolderContent(calendarBackupFolder, false)); - - // find file with modification with date and time between 00:00 and 23:59 - // if more than one file exists, take oldest - Calendar date = Calendar.getInstance(); - date.set(year, month, dayOfMonth); - - // start - date.set(Calendar.HOUR, 0); - date.set(Calendar.MINUTE, 0); - date.set(Calendar.SECOND, 1); - date.set(Calendar.MILLISECOND, 0); - date.set(Calendar.AM_PM, Calendar.AM); - long start = date.getTimeInMillis(); - - // end - date.set(Calendar.HOUR, 23); - date.set(Calendar.MINUTE, 59); - date.set(Calendar.SECOND, 59); - long end = date.getTimeInMillis(); - - OCFile contactsBackupToRestore = null; - List calendarBackupsToRestore = new ArrayList<>(); - - for (OCFile file : backupFiles) { - if (start < file.getModificationTimestamp() && end > file.getModificationTimestamp()) { - // contact - if (MimeTypeUtil.isVCard(file)) { - if (contactsBackupToRestore == null) { - contactsBackupToRestore = file; - } else if (contactsBackupToRestore.getModificationTimestamp() < file.getModificationTimestamp()) { - contactsBackupToRestore = file; - } - } - - // calendars - if (showCalendarBackup && MimeTypeUtil.isCalendar(file)) { - calendarBackupsToRestore.add(file); - } - } - } - - List backupToRestore = new ArrayList<>(); - - if (contactsBackupToRestore != null) { - backupToRestore.add(contactsBackupToRestore); - } - - backupToRestore.addAll(calendarBackupsToRestore); - - - if (backupToRestore.isEmpty()) { - DisplayUtils.showSnackMessage(getView().findViewById(R.id.contacts_linear_layout), - R.string.contacts_preferences_no_file_found); - } else { - final User user = contactsPreferenceActivity.getUser().orElseThrow(RuntimeException::new); - OCFile[] files = new OCFile[backupToRestore.size()]; - Fragment contactListFragment = BackupListFragment.newInstance(backupToRestore.toArray(files), user); - - contactsPreferenceActivity.getSupportFragmentManager(). - beginTransaction() - .replace(R.id.frame_container, contactListFragment, BackupListFragment.TAG) - .addToBackStack(ContactsPreferenceActivity.BACKUP_TO_LIST) - .commit(); - } - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupFragment.kt new file mode 100644 index 000000000000..e216bacaed7d --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupFragment.kt @@ -0,0 +1,726 @@ +/* + * Nextcloud Android client application + * + * @author Mario Danic + * @author TSI-mc + * Copyright (C) 2017 Mario Danic + * Copyright (C) 2017 Nextcloud GmbH. + * Copyright (C) 2023 TSI-mc + * + * 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.fragment.contactsbackup + +import android.Manifest +import android.annotation.SuppressLint +import android.app.DatePickerDialog +import android.app.DatePickerDialog.OnDateSetListener +import android.content.Context +import android.content.Intent +import android.os.AsyncTask +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.CompoundButton +import android.widget.DatePicker +import android.widget.Toast +import com.nextcloud.client.account.User +import com.nextcloud.client.di.Injectable +import com.nextcloud.client.jobs.BackgroundJobManager +import com.owncloud.android.R +import com.owncloud.android.databinding.BackupFragmentBinding +import com.owncloud.android.datamodel.ArbitraryDataProvider +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.RefreshFolderOperation +import com.owncloud.android.ui.activity.ContactsPreferenceActivity +import com.owncloud.android.ui.activity.SettingsActivity +import com.owncloud.android.ui.fragment.FileFragment +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.MimeTypeUtil +import com.owncloud.android.utils.PermissionUtil +import com.owncloud.android.utils.PermissionUtil.checkSelfPermission +import com.owncloud.android.utils.theme.ThemeUtils +import com.owncloud.android.utils.theme.ViewThemeUtils +import third_parties.daveKoeller.AlphanumComparator +import java.util.Calendar +import java.util.Collections +import java.util.Date +import javax.inject.Inject + +@Suppress("TooManyFunctions") +class BackupFragment : FileFragment(), OnDateSetListener, Injectable { + private lateinit var binding: BackupFragmentBinding + + @JvmField + @Inject + var backgroundJobManager: BackgroundJobManager? = null + + @JvmField + @Inject + var themeUtils: ThemeUtils? = null + + @JvmField + @Inject + var arbitraryDataProvider: ArbitraryDataProvider? = null + + @JvmField + @Inject + var viewThemeUtils: ViewThemeUtils? = null + + private var selectedDate: Date? = null + private var calendarPickerOpen = false + private var datePickerDialog: DatePickerDialog? = null + private var contactsCheckedListener: CompoundButton.OnCheckedChangeListener? = null + private var calendarCheckedListener: CompoundButton.OnCheckedChangeListener? = null + private var user: User? = null + private var showSidebar = true + + // flag to check if calendar backup should be shown and backup should be done or not + private var showCalendarBackup = true + private var isCalendarBackupEnabled: Boolean + get() = user?.let { arbitraryDataProvider?.getBooleanValue(it, PREFERENCE_CALENDAR_BACKUP_ENABLED) } ?: false + private set(enabled) { + arbitraryDataProvider!!.storeOrUpdateKeyValue( + user!!.accountName, + PREFERENCE_CALENDAR_BACKUP_ENABLED, + enabled + ) + } + + private var isContactsBackupEnabled: Boolean + get() = user?.let { arbitraryDataProvider?.getBooleanValue(it, PREFERENCE_CONTACTS_BACKUP_ENABLED) } ?: false + private set(enabled) { + arbitraryDataProvider!!.storeOrUpdateKeyValue( + user!!.accountName, + PREFERENCE_CONTACTS_BACKUP_ENABLED, + enabled + ) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + // use grey as fallback for elements where custom theming is not available + if (themeUtils?.themingEnabled(context) == true) { + requireContext().theme.applyStyle(R.style.FallbackThemingTheme, true) + } + + binding = BackupFragmentBinding.inflate(inflater, container, false) + val view: View = binding.root + + setHasOptionsMenu(true) + + if (arguments != null) { + showSidebar = requireArguments().getBoolean(ARG_SHOW_SIDEBAR) + } + + showCalendarBackup = requireContext().resources.getBoolean(R.bool.show_calendar_backup) + + val contactsPreferenceActivity = activity as ContactsPreferenceActivity? + user = contactsPreferenceActivity?.user?.orElseThrow { RuntimeException() } + + setupSwitches(user) + + setupCheckListeners() + setBackupNowButtonVisibility() + + setOnClickListeners() + + contactsPreferenceActivity?.let { + displayLastBackup(it) + applyUserColorToActionBar(it) + } + + setupDates(savedInstanceState) + applyUserColor() + + return view + } + + private fun setupSwitches(user: User?) { + user?.let { + binding.dailyBackup.isChecked = arbitraryDataProvider?.getBooleanValue( + it, + ContactsPreferenceActivity.PREFERENCE_CONTACTS_AUTOMATIC_BACKUP + ) ?: false + } + + binding.contacts.isChecked = isContactsBackupEnabled && checkContactBackupPermission() + binding.calendar.isChecked = isCalendarBackupEnabled && checkCalendarBackupPermission(requireContext()) + binding.calendar.visibility = if (showCalendarBackup) View.VISIBLE else View.GONE + } + + private fun setupCheckListeners() { + binding.dailyBackup.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> + if (checkAndAskForContactsReadPermission()) { + setAutomaticBackup(isChecked) + } + } + + initContactsCheckedListener() + binding.contacts.setOnCheckedChangeListener(contactsCheckedListener) + + initCalendarCheckedListener() + binding.calendar.setOnCheckedChangeListener(calendarCheckedListener) + } + + private fun initContactsCheckedListener() { + contactsCheckedListener = + CompoundButton.OnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> + if (isChecked) { + if (checkAndAskForContactsReadPermission()) { + isContactsBackupEnabled = true + } + } else { + isContactsBackupEnabled = false + } + setBackupNowButtonVisibility() + setAutomaticBackup(binding.dailyBackup.isChecked) + } + } + + private fun initCalendarCheckedListener() { + calendarCheckedListener = + CompoundButton.OnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> + if (isChecked) { + if (checkAndAskForCalendarReadPermission()) { + isCalendarBackupEnabled = true + } + } else { + isCalendarBackupEnabled = false + } + setBackupNowButtonVisibility() + setAutomaticBackup(binding.dailyBackup.isChecked) + } + } + + private fun setBackupNowButtonVisibility() { + binding.backupNow.visibility = + if (binding.contacts.isChecked || binding.calendar.isChecked) View.VISIBLE else View.INVISIBLE + } + + private fun setOnClickListeners() { + binding.backupNow.setOnClickListener { backupNow() } + binding.contactsDatepicker.setOnClickListener { openCleanDate() } + } + + private fun displayLastBackup(contactsPreferenceActivity: ContactsPreferenceActivity) { + val lastBackupTimestamp = user?.let { + arbitraryDataProvider?.getLongValue( + it, + ContactsPreferenceActivity.PREFERENCE_CONTACTS_LAST_BACKUP + ) + } ?: -1L + + if (lastBackupTimestamp == -1L) { + binding.lastBackupWithDate.visibility = View.GONE + } else { + binding.lastBackupWithDate.text = String.format( + getString(R.string.last_backup), + DisplayUtils.getRelativeTimestamp(contactsPreferenceActivity, lastBackupTimestamp) + ) + } + } + + private fun applyUserColorToActionBar(contactsPreferenceActivity: ContactsPreferenceActivity) { + val actionBar = contactsPreferenceActivity.supportActionBar + + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true) + viewThemeUtils?.files?.themeActionBar( + requireContext(), + actionBar, + if (showCalendarBackup) R.string.backup_title else R.string.contact_backup_title + ) + } + } + + private fun setupDates(savedInstanceState: Bundle?) { + if (savedInstanceState != null && savedInstanceState.getBoolean(KEY_CALENDAR_PICKER_OPEN, false)) { + if (savedInstanceState.getInt(KEY_CALENDAR_YEAR, -1) != -1 && savedInstanceState.getInt( + KEY_CALENDAR_MONTH, + -1 + ) != -1 && savedInstanceState.getInt( + KEY_CALENDAR_DAY, -1 + ) != -1 + ) { + val cal = Calendar.getInstance() + cal[Calendar.YEAR] = savedInstanceState.getInt(KEY_CALENDAR_YEAR) + cal[Calendar.MONTH] = savedInstanceState.getInt(KEY_CALENDAR_MONTH) + cal[Calendar.DAY_OF_MONTH] = savedInstanceState.getInt(KEY_CALENDAR_DAY) + selectedDate = cal.time + } + calendarPickerOpen = true + } + } + + private fun applyUserColor() { + viewThemeUtils?.androidx?.colorSwitchCompat(binding.contacts) + viewThemeUtils?.androidx?.colorSwitchCompat(binding.calendar) + viewThemeUtils?.androidx?.colorSwitchCompat(binding.dailyBackup) + + viewThemeUtils?.material?.colorMaterialButtonPrimaryFilled(binding.backupNow) + viewThemeUtils?.material?.colorMaterialButtonPrimaryOutlined(binding.contactsDatepicker) + + viewThemeUtils?.platform?.colorTextView(binding.dataToBackUpTitle) + viewThemeUtils?.platform?.colorTextView(binding.backupSettingsTitle) + } + + override fun onResume() { + super.onResume() + + if (calendarPickerOpen) { + if (selectedDate != null) { + openDate(selectedDate) + } else { + openDate(null) + } + } + + val contactsPreferenceActivity = activity as ContactsPreferenceActivity? + if (contactsPreferenceActivity != null) { + val backupFolderPath = resources.getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR + refreshBackupFolder( + backupFolderPath, + contactsPreferenceActivity.applicationContext, + contactsPreferenceActivity.storageManager + ) + } + } + + private fun refreshBackupFolder( + backupFolderPath: String, + context: Context, + storageManager: FileDataStorageManager + ) { + val task: AsyncTask = + @SuppressLint("StaticFieldLeak") + object : AsyncTask() { + @Deprecated("Deprecated in Java") + override fun doInBackground(vararg path: String): Boolean { + val folder = storageManager.getFileByPath(path[0]) + return if (folder != null) { + val operation = RefreshFolderOperation( + folder, + System.currentTimeMillis(), + false, + false, + storageManager, + user, + context + ) + val result = operation.execute(user, context) + result.isSuccess + } else { + java.lang.Boolean.FALSE + } + } + + @Deprecated("Deprecated in Java") + override fun onPostExecute(result: Boolean) { + if (result) { + val backupFolder = storageManager.getFileByPath(backupFolderPath) + val backupFiles = storageManager + .getFolderContent(backupFolder, false) + Collections.sort(backupFiles, AlphanumComparator()) + if (backupFiles == null || backupFiles.isEmpty()) { + binding.contactsDatepicker.visibility = View.INVISIBLE + } else { + binding.contactsDatepicker.visibility = View.VISIBLE + } + } + } + } + + task.execute(backupFolderPath) + } + + @Deprecated("Deprecated in Java") + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val contactsPreferenceActivity = activity as ContactsPreferenceActivity? + + val retval: Boolean + when (item.itemId) { + android.R.id.home -> { + if (showSidebar) { + if (contactsPreferenceActivity!!.isDrawerOpen) { + contactsPreferenceActivity.closeDrawer() + } else { + contactsPreferenceActivity.openDrawer() + } + } else if (activity != null) { + requireActivity().finish() + } else { + val settingsIntent = Intent(context, SettingsActivity::class.java) + settingsIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP + startActivity(settingsIntent) + } + retval = true + } + + else -> retval = super.onOptionsItemSelected(item) + } + return retval + } + + @Deprecated("Deprecated in Java") + @Suppress("NestedBlockDepth") + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + if (requestCode == PermissionUtil.PERMISSIONS_READ_CONTACTS_AUTOMATIC) { + for (index in permissions.indices) { + if (Manifest.permission.READ_CONTACTS.equals(permissions[index], ignoreCase = true)) { + if (grantResults[index] >= 0) { + // if approved, exit for loop + isContactsBackupEnabled = true + break + } + + // if not accepted, disable again + binding.contacts.setOnCheckedChangeListener(null) + binding.contacts.isChecked = false + binding.contacts.setOnCheckedChangeListener(contactsCheckedListener) + } + } + } + if (requestCode == PermissionUtil.PERMISSIONS_READ_CALENDAR_AUTOMATIC) { + var readGranted = false + var writeGranted = false + for (index in permissions.indices) { + if (Manifest.permission.WRITE_CALENDAR.equals( + permissions[index], + ignoreCase = true + ) && grantResults[index] >= 0 + ) { + writeGranted = true + } else if (Manifest.permission.READ_CALENDAR.equals( + permissions[index], + ignoreCase = true + ) && grantResults[index] >= 0 + ) { + readGranted = true + } + } + if (!readGranted || !writeGranted) { + // if not accepted, disable again + binding.calendar.setOnCheckedChangeListener(null) + binding.calendar.isChecked = false + binding.calendar.setOnCheckedChangeListener(calendarCheckedListener) + } else { + isCalendarBackupEnabled = true + } + } + setBackupNowButtonVisibility() + setAutomaticBackup(binding.dailyBackup.isChecked) + } + + private fun backupNow() { + if (isContactsBackupEnabled && checkContactBackupPermission()) { + startContactsBackupJob() + } + if (showCalendarBackup && isCalendarBackupEnabled && checkCalendarBackupPermission(requireContext())) { + startCalendarBackupJob() + } + DisplayUtils.showSnackMessage( + requireView().findViewById(R.id.contacts_linear_layout), + R.string.contacts_preferences_backup_scheduled + ) + } + + private fun startContactsBackupJob() { + val activity = activity as ContactsPreferenceActivity? + if (activity != null) { + val optionalUser = activity.user + if (optionalUser.isPresent) { + backgroundJobManager!!.startImmediateContactsBackup(optionalUser.get()) + } + } + } + + private fun startCalendarBackupJob() { + val activity = activity as ContactsPreferenceActivity? + if (activity != null) { + val optionalUser = activity.user + if (optionalUser.isPresent) { + backgroundJobManager!!.startImmediateCalendarBackup(optionalUser.get()) + } + } + } + + private fun setAutomaticBackup(enabled: Boolean) { + val activity = activity as ContactsPreferenceActivity? ?: return + val optionalUser = activity.user + if (!optionalUser.isPresent) { + return + } + val user = optionalUser.get() + if (enabled) { + if (isContactsBackupEnabled) { + Log_OC.d(TAG, "Scheduling contacts backup job") + backgroundJobManager?.schedulePeriodicContactsBackup(user) + } else { + Log_OC.d(TAG, "Cancelling contacts backup job") + backgroundJobManager?.cancelPeriodicContactsBackup(user) + } + if (isCalendarBackupEnabled) { + Log_OC.d(TAG, "Scheduling calendar backup job") + backgroundJobManager?.schedulePeriodicCalendarBackup(user) + } else { + Log_OC.d(TAG, "Cancelling calendar backup job") + backgroundJobManager?.cancelPeriodicCalendarBackup(user) + } + } else { + Log_OC.d(TAG, "Cancelling all backup jobs") + backgroundJobManager?.cancelPeriodicContactsBackup(user) + backgroundJobManager?.cancelPeriodicCalendarBackup(user) + } + arbitraryDataProvider?.storeOrUpdateKeyValue( + user.accountName, + ContactsPreferenceActivity.PREFERENCE_CONTACTS_AUTOMATIC_BACKUP, + enabled.toString() + ) + } + + private fun checkAndAskForContactsReadPermission(): Boolean { + val contactsPreferenceActivity = activity as ContactsPreferenceActivity? + + // check permissions + return if (checkSelfPermission(contactsPreferenceActivity!!, Manifest.permission.READ_CONTACTS)) { + true + } else { + // No explanation needed, request the permission. + requestPermissions( + arrayOf(Manifest.permission.READ_CONTACTS), + PermissionUtil.PERMISSIONS_READ_CONTACTS_AUTOMATIC + ) + false + } + } + + private fun checkAndAskForCalendarReadPermission(): Boolean { + val contactsPreferenceActivity = activity as ContactsPreferenceActivity? + + // check permissions + return if (contactsPreferenceActivity?.let { checkCalendarBackupPermission(it) } == true) { + true + } else { + // No explanation needed, request the permission. + requestPermissions( + arrayOf( + Manifest.permission.READ_CALENDAR, + Manifest.permission.WRITE_CALENDAR + ), + PermissionUtil.PERMISSIONS_READ_CALENDAR_AUTOMATIC + ) + false + } + } + + private fun checkCalendarBackupPermission(context: Context): Boolean { + return checkSelfPermission(requireContext(), Manifest.permission.READ_CALENDAR) && checkSelfPermission( + context, Manifest.permission.WRITE_CALENDAR + ) + } + + private fun checkContactBackupPermission(): Boolean { + return checkSelfPermission(requireContext(), Manifest.permission.READ_CONTACTS) + } + + private fun openCleanDate() { + if (checkAndAskForCalendarReadPermission() && checkAndAskForContactsReadPermission()) { + openDate(null) + } + } + + private fun openDate(savedDate: Date?) { + val contactsPreferenceActivity = activity as ContactsPreferenceActivity? + if (contactsPreferenceActivity == null) { + Toast.makeText(context, getString(R.string.error_choosing_date), Toast.LENGTH_LONG).show() + return + } + + val contactsBackupFolderString = resources.getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR + val calendarBackupFolderString = resources.getString(R.string.calendar_backup_folder) + OCFile.PATH_SEPARATOR + val storageManager = contactsPreferenceActivity.storageManager + val contactsBackupFolder = storageManager.getFileByDecryptedRemotePath(contactsBackupFolderString) + val calendarBackupFolder = storageManager.getFileByDecryptedRemotePath(calendarBackupFolderString) + + val backupFiles = storageManager.getFolderContent(contactsBackupFolder, false) + backupFiles.addAll(storageManager.getFolderContent(calendarBackupFolder, false)) + backupFiles.sortWith { o1: OCFile?, o2: OCFile? -> + if (o1 != null && o2 != null) { + o1.modificationTimestamp.compareTo(o2.modificationTimestamp) + } else { + -1 + } + } + + val cal = Calendar.getInstance() + val year: Int + val month: Int + val day: Int + if (savedDate == null) { + year = cal[Calendar.YEAR] + month = cal[Calendar.MONTH] + 1 + day = cal[Calendar.DAY_OF_MONTH] + } else { + year = savedDate.year + month = savedDate.month + day = savedDate.day + } + if (backupFiles.size > 0 && backupFiles[backupFiles.size - 1] != null) { + datePickerDialog = DatePickerDialog(contactsPreferenceActivity, this, year, month, day) + datePickerDialog?.datePicker?.maxDate = backupFiles[backupFiles.size - 1]!! + .modificationTimestamp + datePickerDialog?.datePicker?.minDate = backupFiles[0]!!.modificationTimestamp + datePickerDialog?.setOnDismissListener { selectedDate = null } + datePickerDialog?.setTitle("") + datePickerDialog?.show() + + viewThemeUtils?.platform?.colorTextButtons( + datePickerDialog!!.getButton(DatePickerDialog.BUTTON_NEGATIVE), + datePickerDialog!!.getButton(DatePickerDialog.BUTTON_POSITIVE) + ) + + // set background to transparent + datePickerDialog?.getButton(DatePickerDialog.BUTTON_NEGATIVE)?.setBackgroundColor(0x00000000) + datePickerDialog?.getButton(DatePickerDialog.BUTTON_POSITIVE)?.setBackgroundColor(0x00000000) + } else { + DisplayUtils.showSnackMessage( + requireView().findViewById(R.id.contacts_linear_layout), + R.string.contacts_preferences_something_strange_happened + ) + } + } + + override fun onStop() { + super.onStop() + + datePickerDialog?.dismiss() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + + datePickerDialog?.let { + outState.putBoolean(KEY_CALENDAR_PICKER_OPEN, it.isShowing) + + if (it.isShowing) { + outState.putInt(KEY_CALENDAR_DAY, it.datePicker.dayOfMonth) + outState.putInt(KEY_CALENDAR_MONTH, it.datePicker.month) + outState.putInt(KEY_CALENDAR_YEAR, it.datePicker.year) + } + } + } + + @Suppress("TooGenericExceptionCaught", "NestedBlockDepth", "ComplexMethod", "LongMethod", "MagicNumber") + override fun onDateSet(view: DatePicker, year: Int, month: Int, dayOfMonth: Int) { + val contactsPreferenceActivity = activity as ContactsPreferenceActivity? + if (contactsPreferenceActivity == null) { + Toast.makeText(context, getString(R.string.error_choosing_date), Toast.LENGTH_LONG).show() + return + } + + selectedDate = Date(year, month, dayOfMonth) + val contactsBackupFolderString = resources.getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR + val calendarBackupFolderString = resources.getString(R.string.calendar_backup_folder) + OCFile.PATH_SEPARATOR + val storageManager = contactsPreferenceActivity.storageManager + val contactsBackupFolder = storageManager.getFileByDecryptedRemotePath(contactsBackupFolderString) + val calendarBackupFolder = storageManager.getFileByDecryptedRemotePath(calendarBackupFolderString) + val backupFiles = storageManager.getFolderContent(contactsBackupFolder, false) + backupFiles.addAll(storageManager.getFolderContent(calendarBackupFolder, false)) + + // find file with modification with date and time between 00:00 and 23:59 + // if more than one file exists, take oldest + val date = Calendar.getInstance() + date[year, month] = dayOfMonth + + // start + date[Calendar.HOUR] = 0 + date[Calendar.MINUTE] = 0 + date[Calendar.SECOND] = 1 + date[Calendar.MILLISECOND] = 0 + date[Calendar.AM_PM] = Calendar.AM + val start = date.timeInMillis + + // end + date[Calendar.HOUR] = 23 + date[Calendar.MINUTE] = 59 + date[Calendar.SECOND] = 59 + val end = date.timeInMillis + var contactsBackupToRestore: OCFile? = null + val calendarBackupsToRestore: MutableList = ArrayList() + for (file in backupFiles) { + if (start < file.modificationTimestamp && end > file.modificationTimestamp) { + // contact + if (MimeTypeUtil.isVCard(file)) { + if (contactsBackupToRestore == null) { + contactsBackupToRestore = file + } else if (contactsBackupToRestore.modificationTimestamp < file.modificationTimestamp) { + contactsBackupToRestore = file + } + } + + // calendars + if (showCalendarBackup && MimeTypeUtil.isCalendar(file)) { + calendarBackupsToRestore.add(file) + } + } + } + val backupToRestore: MutableList = ArrayList() + if (contactsBackupToRestore != null) { + backupToRestore.add(contactsBackupToRestore) + } + backupToRestore.addAll(calendarBackupsToRestore) + if (backupToRestore.isEmpty()) { + DisplayUtils.showSnackMessage( + requireView().findViewById(R.id.contacts_linear_layout), + R.string.contacts_preferences_no_file_found + ) + } else { + val user = contactsPreferenceActivity.user.orElseThrow { RuntimeException() } + val files: Array = arrayOfNulls(backupToRestore.size) + + val contactListFragment = BackupListFragment.newInstance(files, user) + + contactsPreferenceActivity.supportFragmentManager.beginTransaction() + .replace(R.id.frame_container, contactListFragment, BackupListFragment.TAG) + .addToBackStack(ContactsPreferenceActivity.BACKUP_TO_LIST) + .commit() + } + } + + companion object { + val TAG = BackupFragment::class.java.simpleName + private const val ARG_SHOW_SIDEBAR = "SHOW_SIDEBAR" + private const val KEY_CALENDAR_PICKER_OPEN = "IS_CALENDAR_PICKER_OPEN" + private const val KEY_CALENDAR_DAY = "CALENDAR_DAY" + private const val KEY_CALENDAR_MONTH = "CALENDAR_MONTH" + private const val KEY_CALENDAR_YEAR = "CALENDAR_YEAR" + const val PREFERENCE_CONTACTS_BACKUP_ENABLED = "PREFERENCE_CONTACTS_BACKUP_ENABLED" + const val PREFERENCE_CALENDAR_BACKUP_ENABLED = "PREFERENCE_CALENDAR_BACKUP_ENABLED" + + @JvmStatic + fun create(showSidebar: Boolean): BackupFragment { + val fragment = BackupFragment() + val bundle = Bundle() + bundle.putBoolean(ARG_SHOW_SIDEBAR, showSidebar) + fragment.arguments = bundle + return fragment + } + } +} diff --git a/app/src/main/res/layout/backup_fragment.xml b/app/src/main/res/layout/backup_fragment.xml index d5b873b8878f..a8467ef4c1d5 100644 --- a/app/src/main/res/layout/backup_fragment.xml +++ b/app/src/main/res/layout/backup_fragment.xml @@ -17,8 +17,8 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . --> - - - - + android:textSize="@dimen/two_line_primary_text_size" /> - - - - + android:gravity="end" + android:orientation="horizontal"> + android:theme="@style/Widget.Material3.Button.IconButton.Filled" /> diff --git a/app/src/main/res/layout/ssl_validator_layout.xml b/app/src/main/res/layout/ssl_validator_layout.xml deleted file mode 100644 index 9b494d9d0468..000000000000 --- a/app/src/main/res/layout/ssl_validator_layout.xml +++ /dev/null @@ -1,443 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/storage_permission_dialog.xml b/app/src/main/res/layout/storage_permission_dialog.xml deleted file mode 100644 index 236f0d3bed5b..000000000000 --- a/app/src/main/res/layout/storage_permission_dialog.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 6c9b9e9e037f..29cd9bfd1a82 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -307,7 +307,7 @@ Urruneko bide-izena Transferitu Deskargatu - Kargatu + Igo Gehitu edo igo Huts egin du fitxategia deskarga-kudeatzailera pasatzean Huts egin du fitxategia inprimatzean @@ -342,7 +342,7 @@ Baimenak behar dira Biltegiratze-baimenak %1$shobeto dabil biltegia atzitzeko baimenekin. Fitxategi guztietarako sarbide osoa aukera dezakezu, edo argazki eta bideoak \"irakurtzeko soilik\" baimena eman. - %1$s-k fitxategiak kudeatzeko baimenak behar ditu fitxategiak kargatzeko. Fitxategi guztietarako sarbide osoa aukera dezakezu, edo \"irakurtzeko soilik\" baimena argazki eta bideoentzat. + %1$s-k fitxategiak kudeatzeko baimenak behar ditu fitxategiak igotzeko. Fitxategi guztietarako sarbide osoa aukera dezakezu, edo \"irakurtzeko soilik\" baimena argazki eta bideoentzat. Helburua egiaztatzen... Garbitzen… Datu-biltegiratze karpeta eguneratzen @@ -424,7 +424,7 @@ %s mm %s s %1$skarpetan - Existitzen diren fitxategiak ere kargatu + Existitzen diren fitxategiak ere igo Igo bakarrik gailua kargatzean /InstantUpload URL baliogabea @@ -433,7 +433,7 @@ Azken babeskopia: %1$s Esteka Esteka-izena - Onartu kargatzea eta edizioa + Onartu igotzea eta edizioa Edizioa Fitxategia jaregitea (igotzeko soilik) Ikustea soilik @@ -522,7 +522,7 @@ Erakutsi zerbitzariak bidalitako push jakinarazpenak: Aipamenak iruzkinetan, urruneko partekatze berrien harrera, administratzaileak argitaratutako argitalpenak etab. Push jakinarazpenak Erakutsi igoeraren egoera - Kargak + Igotzeak Jakinarazpen ikonoa Jakinarazpenik ez Begiratu beranduago, mesedez. @@ -556,13 +556,13 @@ 2012/05/18 12:23 PM gelditu txandakatu - Energia aurrezteko kontrola desgaituz gero, fitxategiak bateria baxu dagoenean kargatu litezke! + Energia aurrezteko kontrola desgaituz gero, fitxategiak bateria baxu dagoenean igo litezke! ezabatua jatorrizko karpetan mantenduko da aplikazioaren karpetara mugitu da Zer egin fitxategia dagoeneko existitzen bada? Galdetu beti - Saltatu karga + Saltatu igotzea Gainidatzi urruneko bertsioa Aldatu izena bertsio berriari Zer egin fitxategia dagoeneko existitzen bada? @@ -610,7 +610,7 @@ Erakutsi ezkutuko fitxategiak Eskuratu iturburu-kodea Datu-biltegiratze karpeta - Kudeatu karpeten kargatze automatikoa + Kudeatu karpeten igotze automatikoa Karpeta lokala Urruneko karpeta Gaia @@ -799,7 +799,7 @@ Sinkronizazioak huts egin du, hasi saioa berriz Fitxategien edukiak dagoeneko sinkronizaturik %1$s karpetaren sinkronizazioa ezin izan da osatu - 1.3.16 bertsiotik aurrera, gailu honetatik kargatzen diren fitxategiak %1$s karpeta lokalera kopiatzen dira, fitxategi bat hainbat konturekin sinkronizatzen denean datuak gal ez daitezen.\n\nAldaketa honen ondorioz, aplikazio honen aurreko bertsioekin kargatutako fitxategi guztiak %2$s karpetara kopiatu dira. Hala ere, kontua sinkronizatzean gertatutako akats batek eragiketa hori osatzea eragotzi du. Fitxategiak dauden bezala utz ditzakezu eta %3$s(r)ako esteka ezabatu, edo fitxategia(k) %1$s karpetara eraman eta %4$s(r)ako esteka mantendu.\n\nBehean zerrendatuta daude fitxategi lokala(k) eta estekatutako %5$s(e)ko urruneko fitxategia(k). + 1.3.16 bertsiotik aurrera, gailu honetatik kargatzen diren fitxategiak %1$s karpeta lokalera kopiatzen dira, fitxategi bat hainbat konturekin sinkronizatzen denean datuak gal ez daitezen.\n\nAldaketa honen ondorioz, aplikazio honen aurreko bertsioekin igotako fitxategi guztiak %2$s karpetara kopiatu dira. Hala ere, kontua sinkronizatzean gertatutako akats batek eragiketa hori osatzea eragotzi du. Fitxategiak dauden bezala utz ditzakezu eta %3$s(r)ako esteka ezabatu, edo fitxategia(k) %1$s karpetara eraman eta %4$s(r)ako esteka mantendu.\n\nBehean zerrendatuta daude fitxategi lokala(k) eta estekatutako %5$s(e)ko urruneko fitxategia(k). Fitxategi lokal batzuk ahaztu dira Fitxategiaren bertsio berriena ekartzen. Aukeratu zer sinkronizatu @@ -810,7 +810,7 @@ Fitxategiak Ezarpenak botoia Konfiguratu karpetak - Berehalako karga erabat aldatu da. Konfiguratu berriro zure karga automatikoa menu nagusitik.\n\nGozatu karga automatiko berri eta hedatuaz. + Berehalako igotzeak guztiz aldatu dira. Konfiguratu berriro zure igotze automatikoa menu nagusitik.\n\nGozatu igotze automatiko berri eta hedatuaz. Ez da multimedia karpetarik aurkitu %1$s-(r)entzat Mota @@ -856,7 +856,7 @@ Kode-zati testu-fitxategia(.txt) Sartu igoko den fitxategi-izena eta fitxategi mota Igo fitxategiak - Kargatu elementuaren ekintza botoia + Elementua igotzeko ekintza botoia Ezabatu Ezin dira fitxategiak igo Edukiak igo edo auto-igotzea aktiba ezazu. @@ -875,7 +875,7 @@ %1$s ez dago baimendua jasotako fitxategia irakurtzeko Fitxategia ezin izan da aldi baterako karpetan kopiatu. Saiatu berriro bidaltzen. Kargatzeko hautatutako fitxategia ez da aurkitu. Mesedez, egiaztatu fitxategia existitzen dela. - Fitxategi hau ezin da kargatu + Fitxategi hau ezin da igo Ez dago fitxategirik kargatzeko Karpetaren izena Aukeratu karga-karpeta diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 6681a4f4aac4..358e5a9d7b5e 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -30,9 +30,11 @@ アクティビティ 別のリンクを作成 新規公開共有リンクを追加 + 新しいセキュアなファイルドロップを追加 %1$s に追加 高度な設定 再共有を許可 + ダッシュボードから一つのウィジェットを表示 %s の中を検索 関連付けられたアカウントが見つかりません! アクセスに失敗しました: %1$s @@ -85,11 +87,14 @@ カレンダー 証明書の読み込みに問題がありました。 開発バージョンの変更履歴 + 後で確認するか、再読込をしてください チェックボックス ローカルフォルダーを選択… + 場所を選択 リモートフォルダを選択… テンプレートを選択してファイル名を入力してください。 保存するファイルを選択 + ウィジェットを選択 通知の削除に失敗 メッセージを消去 ステータスメッセージの有効期限 @@ -149,6 +154,7 @@ ローカルファイル 両方のバージョンを選択した場合、ローカルファイルはファイル名に数字が追加されます。 サーバーファイル + 連絡先のバックアップ 連絡先リストのユーザーアイコン 許可がありません、インポートできません。 連絡先 @@ -193,13 +199,20 @@ 重複をチェックしませんでした このスマートフォンでは、ダイジェストアルゴリズムが利用できません。 ダイレクトリンクからのログインに失敗しました! + %1$s で %2$s へログインする 無効 閉じる 通知を閉じる 取り込み中 + 複数の画像 + PDFファイル + エクスポートする種類を選択 + PDFの生成に失敗しました + PDFを生成中... 完了 消去しない ローカルファイルが作成できません + ローカルファイルに対して無効なファイル名 最新の開発バージョンをダウンロード %1$sをダウンロードできませんでした ダウンロード失敗、要 再ログイン @@ -229,10 +242,14 @@ %2$s 中%1$s が使われています。 %1$s使用中 自動アップロード + E2E暗号化が未設定 + インターネット接続なしには不可能です さらに表示 ノート トーク + もっと Nextcloud アプリを見る Nextcloud ノート + Nextcloud Talk 暗号化設定 暗号化を設定する 復号化中… @@ -262,7 +279,9 @@ 問題を報告しますか? (GitHubのアカウントが必要です) ファイルの取得中にエラーが発生しました テンプレートの取得中にエラーが発生しました + 暗号化設定ダイアログの表示エラー カメラ起動エラー + 文書スキャンの開始エラー アカウント ジョブの名前 進捗状況 @@ -293,6 +312,7 @@ UIの更新に失敗しました お気に入りに追加 お気に入り + ファイル名が既に存在します 削除 ファイルのアクティビティ取得エラー 詳細のロードに失敗しました @@ -358,6 +378,7 @@ ファイル名 あなたのデータをセキュアなままコントロールしましょう セキュアなコラボレーションとファイル交換 + 使いやすいWebメールやカレンダーや & 連絡先 画面共有やオンラインミーティングやウェブ会議 フォルダーはすでに存在します 作成 @@ -383,6 +404,18 @@ パスワード サーバーが利用できません 自分のサーバーをホストする + ダッシュボードウィジェットのアイコン + 編集された + 左右反転 + 上下反転 + 反時計回りに回す + 時計回りに回す + 画像の編集が不可能です + ファイルの詳細 + ISO %s + %s MP + %s mm + %s 秒 フォルダー%1$sの中で 既存のファイルもアップロード 充電中のみアップロード @@ -403,12 +436,14 @@ ローカルファイルシステムにファイルが見つかりません %1$s/%2$s これ以上フォルダーがありません。 + フォルダーの配置 有効期限: %1$s ファイルをロック %1$sによりロック %1$sアプリによりロック %1$s アンドロイドアプリログ ログを送信するためのアプリが見つかりません。メールクライアントをインストールしてください。 + %1$sとしてログインしました ログイン %1$sをWeb画面でブラウザーで開くときのURL ログを消去 @@ -459,6 +494,8 @@ リンクを処理するアプリがありません カレンダーがありません メールアドレスの利用可能なアプリはありません + アイテムがありません + マップを処理するアプリケーションがありません 利用できるアカウントは1つだけです PDFを処理するアプリケーションがありません 選択されたファイルの送信で利用可能なアプリがありません @@ -497,11 +534,13 @@ パスコードを削除しました パスコードを保存しました パスコードが正しくありません + パスワード保護されたPDFファイルを開くことは出来ません。外部ビューワを使ってください ズームするにはページをタップ 許可 拒否 ファイルをダウンロードとアップロードする追加の権限が必要です。 画像を設定するアプリが見つかりませんでした + ホームスクリーンにピン留めする 389 KB placeholder.txt 12:23:45 @@ -531,6 +570,7 @@ もっと見る カレンダーと連絡先を毎日バックアップ 連絡先のデイリーバックアップ + End-to-end 暗号化を設定中! E2Eニーモニック ニーモニックを表示するには、デバイスクレデンシャルを有効にしてください。 メディアのスキャン結果通知を表示する @@ -540,7 +580,9 @@ インプリント 元のファイルの扱い… 元のファイルになります… + 日付を基にしたサブフォルダーに保存 サブフォルダーを利用 + このクライアントに End-to-End 暗号化を追加 ライセンス アプリパスコード デバイスの資格情報が有効です @@ -551,6 +593,7 @@ パスコード アカウント管理 友達にすすめる + end-to-end 暗号化を設定 隠しファイルを表示 ソースコードを入手 データ保存フォルダー @@ -574,6 +617,7 @@ デバイスで %1$s をお試しください あなたのデバイスで %1$s を使用してください。\nダウンロードはこちらです: %2$s %1$s または %2$s + コンテンツを更新 再読み込み (リモート) ファイルが見つかりません! @@ -594,6 +638,7 @@ ファイルを取得中... ドキュメントのロードに失敗しました。 QRコードを用いてログイン + ページをスキャン あなたのデータを保護 自己ホスト型の生産性 閲覧と共有 @@ -608,6 +653,7 @@ DAVx5で同期 検索結果の取得中にエラーが発生しました すべて選択 + メディア用のフォルダーの設定 テンプレートを選択してください テンプレートを選択する 送信 @@ -643,6 +689,7 @@ パスワード保護 編集可能 ファイルを転送 + セキュアなファイルドロップ 閲覧のみ 共有権限 %1$s (リモート) @@ -662,6 +709,8 @@ リンク経由で共有 %1$sと共有中 共有を追加できませんでした + 写真を表示 + 動画を表示 他のサービスでサインアップ %1$s があなたのNextcloudアカウント %2$s にアクセスできるようにしますか? ソート @@ -716,9 +765,13 @@ 内部ストリーミングは不可能 代わりにメディアをダウンロードするか、外部アプリを使用してください。 ストリクトモード:HTTP接続が許可されていません! + 念/月/日 + 年/月 \"%1$s\" があなたに共有されました %1$s は \"%2$s\" をあなたと共有しました + 写真のみ + 動画のみ 提案 競合が見つかりました フォルダー %1$s はもう存在しません @@ -752,6 +805,7 @@ サムネイル 既存ファイルのサムネイル 新規ファイルのサムネイル + ローディング中 期待した時間より長くかかっています 今日 ゴミ箱 削除されたファイルはありません @@ -866,6 +920,7 @@ スキップ %1$sの新機能 あなたのステータスは? + ウィジェットは %1$s 25 以上でのみ利用可能です  利用できません メールを送信 データ保存フォルダーが存在しません! @@ -888,6 +943,18 @@ 重複する応募が%d件見つかりました。 + + エクスポートされた %d ファイル + + + %dファイルのエクスポートに失敗しました + + + エラーのため、%d ファイルはエクスポートされ、他のファイルはスキップされました + + + %dファイルがエクスポートされます。詳細は通知を確認してください。 + %1$d フォルダ diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 2cb934c28dc6..d1bec04cd0f9 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -420,7 +420,7 @@ 마지막 백업: %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 c9d548abe2db..1c51ee591b09 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -125,7 +125,7 @@ Mudar para a conta Sim Teste a versão de desenvolvimento - Inclui todas as futuras e mais recentes funcionalidades. Falhas/erros podem ocorrer, se e quando tal acontecer, por favor, reporte as suas descobertas. + Isto inclui todas as futuras e mais recentes funcionalidades. Podem ocorrer falhas/erros, se e quando tal acontecer, por favor, reporte as suas descobertas. fórum Ajude outros em Reveja, emende e escreva o código, consulte %1$s para detalhes. @@ -137,12 +137,12 @@ Obter versão candidata a lançamento a partir da aplicação F-Droid Obter versão candidata a lançamento a partir da loja Google Play Versão candidata a lançamento - A versão candidata a lançamento (RC) é um snapshot da próxima versão e é espectável que seja estável. Através do teste da sua configuração individual pode ajudar-nos a assegurá-lo. Para testar, inscreva-se na loja Play ou verifique a secção \"Version\" do F-Droid. + A versão candidata de lançamento (RC) é um \'\'snapshot\'\' da próxima versão e é expetável que seja estável. Se testar a sua configuração individual pode ajudar-nos a assegurá-lo. Para testar, inscreva-se na loja Play ou consulte a secção \"Version\" do F-Droid. Encontrou um erro? Ocorrências estranhas? Ajude-nos, testando Reportar um problema no Github Configure - Remove localmente a criptografia + Remove encriptação local Deseja realmente apagar %1$s? Quer realmente remover os itens seleccionados? Deseja realmente apagar %1$s e o seu conteúdo? @@ -152,7 +152,7 @@ Ficheiro em conflito %1$s Ficheiro local Se selecionou ambas as versões, o ficheiro local terá um número acrescentado ao seu nome. - Arquivo do servidor + Ficheiro do servidor Cópia de segurança dos contactos Ícone de utilizador de lista de contactos Sem permissão concedida, nada para importar. @@ -190,7 +190,7 @@ Apagar entradas Eliminar hiperligação Desseleccionado tudo - Nome do arquivo de destino + Nome do ficheiro de destino Nova versão disponível Nenhuma informação disponível. Nenhuma nova versão disponível @@ -202,12 +202,16 @@ Desativar Dispensar Dispensar notificação - Apresenta a sua frase-chave de 12 palavras + Exibe a sua frase-chave de 12 palavras Não incomodar Múltiplas imagens + Ficheiro PDF + Escolher tipo de exportação + Geração de PDF falhou + A gerar PDF... Concluído Não limpar - Não é possível criar ficheiro local + Não é possível criar o ficheiro local Transferir a última versão de desenvolvimento Não foi possível transferir %1$s A transferência falhou, inicie novamente a sessão @@ -238,7 +242,11 @@ %1$s utilizado Carregamento automático Mais + Notas Falar + Mais Aplicações Nextcloud + Nextcloud Notes + Nextcloud Talk Definir como encriptado Definir encriptação Decryption… @@ -255,15 +263,19 @@ Definir encriptação Não foi possível guardar as chaves, por favor, tente novamente. Erro ao encriptar. Palavra-passe errada? + Inserir nome do ficheiro de destino Por favor, introduza um nome para o ficheiro Não foi possível copiar %1$s para a pasta local %2$s Erro crítico: impossível concluir a operação Erro ao escolher a data Erro ao comentar ficheiro %1$s crachou + Erro ao criar o ficheiro com o modelo + Erro ao mostrar as ações do ficheiro Relatório Erro ao obter o ficheiro Erro ao obter modelos + Erro ao mostrar a janela da configuração de encriptação! Erro ao iniciar câmara Contas Nome do Trabalho @@ -289,11 +301,12 @@ Enviar Adicionar ou enviar Falhou a passagem do ficheiro ao gestor de transferências - Falhou a impressão do ficheiro + Não foi possível imprimir o ficheiro Falha ao iniciar o editor Falha ao atualizar a IU Adicionar aos favoritos Favorito + O nome do ficheiro já existe Apagar Erro ao obter as atividades para o ficheiro Falha ao carregar detalhes @@ -307,10 +320,10 @@ Sem resultados nesta pasta Sem resultados Não está aqui nada. Pode adicionar uma pasta. - Ficheiros e pastas que foram descarregados aparecem aqui. + Os ficheiros e as pastas que foram transferidos serão mostrados aqui. Não foi encontrado nenhum arquivo modificado nos últimos 7 dias Poderá estar numa pasta diferente? - Ficheiros e pastas que partilhou aparecem aqui. + Os ficheiros e as pastas que partilhou serão mostrados aqui. Ainda sem partilhas Nenhum resultado encontrado para a sua consulta pasta @@ -318,6 +331,7 @@ Nenhuma aplicação para usar este tipo de ficheiro. há segundos Permissões necessárias + Permissões de armazenamento %1$s precisa de permissões de gestão de ficheiros para carregar ficheiros. Pode escolher acesso total a todos os ficheiros ou acesso só de leitura a fotografias e vídeos. Verificando destino… Limpando… @@ -383,11 +397,11 @@ Servidor não disponível Hospede o seu próprio servidor Ícone para lista vazia - Ícone do widget do painel de controlo - Ícone do widget da entrada + Ícone do \'\'widget\'\' do painel de controlo + Ícone do \'\'widget\'\' da entrada editado - Virar horizontalmente - Virar verticalmente + Inverter horizontalmente + Inverter verticalmente Rodar no sentido anti-horário Rodar no sentido horário Não é possível editar a imagem. @@ -404,7 +418,7 @@ /Envio Instantâneo URL inválido Invisível - Nome não pode ficar em branco + O nome não pode ficar em branco Última cópia de segurança: %1$s Hiperligação Nome da hiperligação @@ -418,9 +432,14 @@ Ficheiros não encontrados no sistema de ficheiros local %1$s/%2$s Não existem mais pastas. + Localizar pasta + Expira: %1$s Bloquear ficheiro + Bloqueado por %1$s + Bloqueado por aplicação %1$s %1$s registos de aplicação Android Não foi encontrada nenhuma aplicação para o envio de registos. Por favor, Instale um cliente de correio eletrónico. + Autenticado como %1$s Iniciar Sessão A hiperligação para a sua interface da Web %1$s quando a abre no seu navegador. Eliminar registos @@ -467,10 +486,14 @@ vídeo Nova notificação Uma nova versão foi criada + Sem ações para este utilizador Nenhuma aplicação disponível para lidar com links O calendário não existe + Sem itens Só é permitida uma conta. Nenhuma aplicação disponível para lidar com PDF + Nenhuma aplicação disponível para enviar os ficheiros selecionados + Por favor, selecione pelo menos uma permissão para partilhar. Enviar Não foi possível enviar a nota Ícone de nota @@ -509,6 +532,8 @@ Negar Permissões adicionais são necessárias para enviar e transferir ficheiros. Nenhuma aplicação encontrada para definir a imagem + Afixar no ecrã Início + Abrir %1$s 389 KB placeholder.txt 12:23:45 @@ -548,7 +573,7 @@ Informação O ficheiro original será… O ficheiro original será… - Armazenar em subpastas com base na data + Guardar nas subpastas com base na data Usar Subpastas Opções de subpasta Licença @@ -561,6 +586,7 @@ Código Gerir contas Recomendar a um amigo + Remover encriptação localmente Configurar a encriptação ponta-a-ponta Mostrar alternador de aplicações Sugestões de aplicações Nextcloud no cabeçalho da navegação @@ -639,6 +665,7 @@ Defina a mensagem de estado Durante a configuração da encriptação ponta-a-ponta, receberá uma mnemónica aleatória de 12 palavras, de que necessitará para abrir os seus ficheiros noutros dispositivos. Ela só será guardada neste dispositivo e pode ser mostrada novamente neste ecrã. Por favor, anote-a num local seguro! Partilhar + Hiperligação de Partilhar e Copiar Partilha %1$s Expira %1$s @@ -679,6 +706,8 @@ Partilhado via hiperligação Partilhado consigo por %1$s Adição de destinatário da partilha falhou + Mostrar fotografias + Mostrar vídeos Registar com fornecedor Permitir que %1$s aceda à sua conta Nextcloud %2$s? Ordenar por @@ -731,9 +760,13 @@ Transmita com… Transmissão interna não é possível Em vez disso, por favor, transfira mediateca ou utilize uma aplicação externa. + Ano/Mês/Dia + Ano/Mês + Ano \"%1$s\" foi partilhado consigo %1$s partilhou \"%2$s\" consigo - Fotos & videos + Apenas fotografias + Fotografias e vídeos Apenas vídeos Sugerir Encontrados conflitos @@ -784,12 +817,12 @@ Aproveite o novo e melhorado envio automático. Encriptação não definida Remover dos favoritos Ocorreu um erro enquanto tentava remover a partilha deste ficheiro ou pasta. - Impossível eliminar a partilha. Verifique se o ficheiro existe. + Não é possível remover a partilha. Por favor, verifique se o ficheiro existe. para cancelar a partilha deste ficheiro Cancelamento da partilha falhou Acesso através de domínio não confiável. Por favor, verifique a documentação para mais informações. - Erro ao tentar modificar a partilha. - Impossível modificar. Verifique se o ficheiro existe. + Ocorreu um erro enquanto tentava atualizar a partilha. + Não é possível atualizar. Por favor, verifique se o ficheiro existe. para atualizar esta partilha Actualização da partilha falhou Não é possível criar ficheiro local @@ -828,7 +861,7 @@ Aproveite o novo e melhorado envio automático. Escolha uma pasta para envio Não foi possível enviar %1$s O envio falhou, inicie novamente a sessão - Exite um conflito com o ficheiro enviado + Conflito de envio do ficheiro Escolha qual das versões manter %1$s Envio falhou Opção de envio: @@ -900,6 +933,26 @@ Aproveite o novo e melhorado envio automático. Falha ao copiar %1$d ficheiros da pasta %2$s para Falha ao copiar %1$d ficheiros da pasta %2$s para + + Processada %d entrada. + Processadas %dentradas. + Processadas %d entradas. + + + Encontrada %d entrada duplicada. + Encontradas %d entradas duplicadas. + Encontradas %d entradas duplicadas. + + + Exportado %d ficheiro + Exportados %d ficheiros + Exportados %d ficheiros + + + Falhou exportação de %d ficheiro + Falhou exportação de %d ficheiros + Falhou exportação de %d ficheiros + %1$d pasta %1$d pastas diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 17ae890a4b19..c592f9757130 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -653,6 +653,7 @@ Đặt mật khẩu Mật khẩu được bảo vệ Có thể chỉnh sửa + Thả file Chỉ xem Quyền kho %1$s (từ xa) diff --git a/app/src/main/res/values/dims.xml b/app/src/main/res/values/dims.xml index 3805116abcbb..6b3fe89a1e9c 100644 --- a/app/src/main/res/values/dims.xml +++ b/app/src/main/res/values/dims.xml @@ -79,7 +79,6 @@ 20dp 12sp 20dp - 180dp 60dp 12sp 12dp @@ -134,6 +133,8 @@ 16sp 18sp 24dp + 160dp + 4 12dp 50dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6b758fa380d7..00883583ec32 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -799,8 +799,6 @@ Deny Allow Note to recipient - Send - Send note to recipient Could not send note Note No app available to handle links diff --git a/scripts/analysis/analysis-wrapper.sh b/scripts/analysis/analysis-wrapper.sh index 8770f1d2401b..81e04ec2e159 100755 --- a/scripts/analysis/analysis-wrapper.sh +++ b/scripts/analysis/analysis-wrapper.sh @@ -128,7 +128,7 @@ else # check for NotNull if [[ $(grep org.jetbrains.annotations app/src/main/* -irl | wc -l) -gt 0 ]] ; then - notNull="org.jetbrains.annotations.NotNull is used. Please use androidx.annotation.NonNull instead.

" + notNull="org.jetbrains.annotations.* is used. Please use androidx.annotation.* instead.

" fi bodyContent="$codacyResult $lintResult $spotbugsResult $checkLibraryMessage $lintMessage $spotbugsMessage $gplayLimitation $notNull" diff --git a/scripts/analysis/lint-results.txt b/scripts/analysis/lint-results.txt index 31824795b1a7..2c0dfa2b0d8a 100644 --- a/scripts/analysis/lint-results.txt +++ b/scripts/analysis/lint-results.txt @@ -1,2 +1,2 @@ DO NOT TOUCH; GENERATED BY DRONE - Lint Report: 80 warnings + Lint Report: 79 warnings