diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..bc7cbef --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only +codecov: + branch: master + ci: + - drone.nextcloud.com + notify: + after_n_builds: 2 + +coverage: + precision: 2 + round: down + range: "70...100" + status: + project: + default: + threshold: 0.5 + +comment: + layout: "header, diff, changes, uncovered, tree" + behavior: default + require_changes: true + after_n_builds: 2 + +github_checks: + annotations: false + +ignore: + - "app/src/main/res/values*/*" + diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..e285b0c --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,20 @@ +FROM ubuntu:noble@sha256:c35e29c9450151419d9448b0fd75374fec4fff364a27f176fb458d472dfc9e54 + +ARG DEBIAN_FRONTEND=noninteractive +ENV ANDROID_HOME=/usr/lib/android-sdk + +RUN apt-get update -y +RUN apt-get install -y unzip wget openjdk-17-jdk vim + +RUN wget https://dl.google.com/android/repository/commandlinetools-linux-6858069_latest.zip -O /tmp/commandlinetools.zip +RUN cd /tmp && unzip commandlinetools.zip +RUN mkdir -p /usr/lib/android-sdk/cmdline-tools/ +RUN cd /tmp/ && mv cmdline-tools/ latest/ && mv latest/ /usr/lib/android-sdk/cmdline-tools/ +RUN mkdir /usr/lib/android-sdk/licenses/ +RUN chmod -R 755 /usr/lib/android-sdk/ +RUN mkdir -p "$HOME/.gradle" && \ + echo "org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g" > "$HOME/.gradle/gradle.properties" && \ + echo "org.gradle.caching=true" >> "$HOME/.gradle/gradle.properties" && \ + echo "org.gradle.parallel=true" >> "$HOME/.gradle/gradle.properties" && \ + echo "org.gradle.configureondemand=true" >> "$HOME/.gradle/gradle.properties" && \ + echo "kapt.incremental.apt=true" >> "$HOME/.gradle/gradle.properties" \ No newline at end of file diff --git a/.devcontainer/Dockerfile.license b/.devcontainer/Dockerfile.license new file mode 100644 index 0000000..d078384 --- /dev/null +++ b/.devcontainer/Dockerfile.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 0000000..60bcf90 --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,9 @@ + +# Instructions + +1. Start a DevContainer either on GitHub Codespaces or locally in VSCode. +2. Accept all licenses by running `yes | /usr/lib/android-sdk/cmdline-tools/latest/bin/sdkmanager --licenses`. +3. You can now build the app using `./gradlew clean build`. diff --git a/.devcontainer/devcontainer.env b/.devcontainer/devcontainer.env new file mode 100644 index 0000000..369163c --- /dev/null +++ b/.devcontainer/devcontainer.env @@ -0,0 +1,3 @@ +ANDROID_HOME=/usr/lib/android-sdk +JAVA_OPTS="-Xmx8192M" +GRADLE_OPTS="-Dorg.gradle.daemon=true" diff --git a/.devcontainer/devcontainer.env.license b/.devcontainer/devcontainer.env.license new file mode 100644 index 0000000..d078384 --- /dev/null +++ b/.devcontainer/devcontainer.env.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..a13d6f9 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,4 @@ +{ + "name": "NextcloudAndroid", + "dockerFile": "Dockerfile", +} diff --git a/.devcontainer/devcontainer.json.license b/.devcontainer/devcontainer.json.license new file mode 100644 index 0000000..d078384 --- /dev/null +++ b/.devcontainer/devcontainer.json.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..1541f7f --- /dev/null +++ b/.drone.yml @@ -0,0 +1,190 @@ +--- +kind: pipeline +type: docker +name: tests-stable + +# SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + + +steps: + - name: gplay + image: ghcr.io/nextcloud/continuous-integration-android8:4 + privileged: true + environment: + LOG_USERNAME: + from_secret: LOG_USERNAME + LOG_PASSWORD: + from_secret: LOG_PASSWORD + GIT_USERNAME: + from_secret: GIT_USERNAME + GITHUB_TOKEN: + from_secret: GIT_TOKEN + commands: + - scripts/checkIfRunDrone.sh $DRONE_PULL_REQUEST || exit 0 + - emulator -avd android -no-snapshot -gpu swiftshader_indirect -no-window -no-audio -skin 500x833 & + - sed -i s'#false#true#'g app/src/main/res/values/setup.xml + - ./gradlew assembleGplayDebugAndroidTest + - scripts/wait_for_emulator.sh + - ./gradlew installGplayDebugAndroidTest + - scripts/wait_for_server.sh "server" + - scripts/deleteOldComments.sh "stable" "IT" $DRONE_PULL_REQUEST + - ./gradlew createGplayDebugCoverageReport -Pcoverage -Pandroid.testInstrumentationRunnerArguments.notAnnotation=com.owncloud.android.utils.ScreenshotTest || scripts/uploadReport.sh $LOG_USERNAME $LOG_PASSWORD $DRONE_BUILD_NUMBER "stable" "IT" $DRONE_PULL_REQUEST + +services: + - name: server + image: ghcr.io/nextcloud/continuous-integration-shallow-server:latest # also change in updateScreenshots.sh + environment: + EVAL: true + SERVER_VERSION: 'stable30' + commands: + - BRANCH="$SERVER_VERSION" /usr/local/bin/initnc.sh + - echo 127.0.0.1 server >> /etc/hosts + - apt-get update && apt-get install -y composer + - su www-data -c "OC_PASS=user1 php /var/www/html/occ user:add --password-from-env --display-name='User One' user1" + - su www-data -c "OC_PASS=user2 php /var/www/html/occ user:add --password-from-env --display-name='User Two' user2" + - su www-data -c "OC_PASS=user3 php /var/www/html/occ user:add --password-from-env --display-name='User Three' user3" + - su www-data -c "php /var/www/html/occ user:setting user2 files quota 1G" + - su www-data -c "php /var/www/html/occ group:add users" + - su www-data -c "php /var/www/html/occ group:adduser users user1" + - su www-data -c "php /var/www/html/occ group:adduser users user2" + - su www-data -c "git clone --depth 1 -b $SERVER_VERSION https://github.com/nextcloud/activity.git /var/www/html/apps/activity/" + - su www-data -c "php /var/www/html/occ app:enable activity" + - su www-data -c "git clone --depth 1 -b $SERVER_VERSION https://github.com/nextcloud/text.git /var/www/html/apps/text/" + - su www-data -c "php /var/www/html/occ app:enable text" + - su www-data -c "git clone --depth 1 -b $SERVER_VERSION https://github.com/nextcloud/end_to_end_encryption.git /var/www/html/apps/end_to_end_encryption/" + - su www-data -c "php /var/www/html/occ app:enable end_to_end_encryption" + - su www-data -c "git clone --depth 1 -b $SERVER_VERSION https://github.com/nextcloud/photos.git /var/www/html/apps/photos/" + - su www-data -c "cd /var/www/html/apps/photos; composer install --no-dev" + - su www-data -c "php /var/www/html/occ app:enable -f photos" + - su www-data -c "php /var/www/html/occ config:system:set ratelimit.protection.enabled --value false --type bool" + - /usr/local/bin/run.sh + +trigger: + branch: + - master + - stable-* + event: + - push + - pull_request +--- +kind: pipeline +type: docker +name: tests-master + +steps: + - name: gplay + image: ghcr.io/nextcloud/continuous-integration-android8:4 + privileged: true + environment: + LOG_USERNAME: + from_secret: LOG_USERNAME + LOG_PASSWORD: + from_secret: LOG_PASSWORD + GIT_USERNAME: + from_secret: GIT_USERNAME + GITHUB_TOKEN: + from_secret: GIT_TOKEN + commands: + - scripts/checkIfRunDrone.sh $DRONE_PULL_REQUEST || exit 0 + - emulator -avd android -no-snapshot -gpu swiftshader_indirect -no-window -no-audio -skin 500x833 & + - sed -i s'#false#true#'g app/src/main/res/values/setup.xml + - scripts/runCombinedTest.sh $DRONE_PULL_REQUEST $LOG_USERNAME $LOG_PASSWORD $DRONE_BUILD_NUMBER + +services: + - name: server + image: ghcr.io/nextcloud/continuous-integration-shallow-server:latest # also change in updateScreenshots.sh + environment: + EVAL: true + commands: + - /usr/local/bin/initnc.sh + - echo 127.0.0.1 server >> /etc/hosts + - apt-get update && apt-get install -y composer + - su www-data -c "OC_PASS=user1 php /var/www/html/occ user:add --password-from-env --display-name='User One' user1" + - su www-data -c "OC_PASS=user2 php /var/www/html/occ user:add --password-from-env --display-name='User Two' user2" + - su www-data -c "OC_PASS=user3 php /var/www/html/occ user:add --password-from-env --display-name='User Three' user3" + - su www-data -c "php /var/www/html/occ user:setting user2 files quota 1G" + - su www-data -c "php /var/www/html/occ group:add users" + - su www-data -c "php /var/www/html/occ group:adduser users user1" + - su www-data -c "php /var/www/html/occ group:adduser users user2" + - su www-data -c "git clone --depth 1 -b master https://github.com/nextcloud/activity.git /var/www/html/apps/activity/" + - su www-data -c "php /var/www/html/occ app:enable activity" + - su www-data -c "git clone --depth 1 -b main https://github.com/nextcloud/text.git /var/www/html/apps/text/" + - su www-data -c "php /var/www/html/occ app:enable text" + - su www-data -c "git clone --depth 1 -b master https://github.com/nextcloud/end_to_end_encryption/ /var/www/html/apps/end_to_end_encryption/" + - su www-data -c "php /var/www/html/occ app:enable end_to_end_encryption" + - su www-data -c "git clone --depth 1 https://github.com/nextcloud/photos.git /var/www/html/apps/photos/" + - su www-data -c "cd /var/www/html/apps/photos; composer install --no-dev" + - su www-data -c "php /var/www/html/occ app:enable -f photos" + - su www-data -c "php /var/www/html/occ config:system:set ratelimit.protection.enabled --value false --type bool" + - /usr/local/bin/run.sh + +trigger: + branch: + - master + - stable-* + event: + - push + - pull_request + +--- +kind: pipeline +type: docker +name: allScreenshots + +steps: + - name: runAllScreenshots + image: ghcr.io/nextcloud/continuous-integration-android8:4 + privileged: true + environment: + GIT_USERNAME: + from_secret: GIT_USERNAME + GITHUB_TOKEN: + from_secret: GIT_TOKEN + LOG_USERNAME: + from_secret: LOG_USERNAME + LOG_PASSWORD: + from_secret: LOG_PASSWORD + commands: + - emulator -avd android -no-snapshot -gpu swiftshader_indirect -no-window -no-audio -skin 500x833 & + - sed -i s'#false#true#'g app/src/main/res/values/setup.xml + - sed -i s'#showOnlyFailingTestsInReports = ciBuild#showOnlyFailingTestsInReports = false#' build.gradle.kts + - scripts/wait_for_emulator.sh + - scripts/runAllScreenshotCombinations noCI false + - scripts/screenshotSummary.sh + - name: notify + image: drillster/drone-email + settings: + port: 587 + from: nextcloud-drone@kaminsky.me + recipients_only: true + username: + from_secret: EMAIL_USERNAME + password: + from_secret: EMAIL_PASSWORD + recipients: + from_secret: EMAIL_RECIPIENTS + host: + from_secret: EMAIL_HOST + when: + event: + - push + status: + - failure + branch: + - master + - stable-* +trigger: + event: + - cron + cron: + - allscreenshots +--- +kind: secret +name: GIT_TOKEN +data: XIoa9IYq+xQ+N5iln8dlpWv0jV6ROr7HuE24ioUr4uQ8m8SjyH0yognWYLYLqnbTKrFWlFZiEMQTH/sZiWjRFvV1iL0= +--- +kind: signature +hmac: b4568fe767026f67cca8c416c20cc522fd1d06941c836ca7eb4955682855d237 + +... diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..67e5fa3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,51 @@ +# .editorconfig + +# see http://EditorConfig.org + +# SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + +# This is the file in the root of the project. +# For sub folders you can have other files that override only some settings. +# For these, this settings should be false. +root=true + +[*] +max_line_length=120 +# use spaces, not tabs. +indent_style=space +indent_size=4 + +[*.yml] +max_line_length=150 + +charset=utf-8 + +# Trimming is good for consistency +trim_trailing_whitespace=true +# I've seen cases where a missing new_line was ignored on *nix systems. +# Never again with this setting! +insert_final_newline=true + +[*.properties] +# Exception for Java properties files should be encoded latin1 (aka iso8859-1) +charset=latin1 + +[*.{cmd,bat}] +# batch files on Windows should stay with CRLF +end_of_line=crlf + +[*.md] +trim_trailing_whitespace=false + +[.drone.yml] +indent_size=2 + +[*.{kt,kts}] +ktlint_code_style = android_studio +# IDE does not follow this Ktlint rule strictly, but the default ordering is pretty good anyway, so let's ditch it +ktlint_standard_import-ordering = disabled +ktlint_standard_no-consecutive-comments = disabled +ktlint_function_naming_ignore_when_annotated_with = Composable +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5924755 --- /dev/null +++ b/.gitignore @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only +# built application files +*.apk +*.ap_ +*.aab + +# files for the dex VM +*.dex + +# Java class files +*.class + +# generated files +bin/ +build/ +*.iml +gen/ +target/ + +# Local configuration files (sdk path, etc) +local.properties +tests/local.properties + +# Mac .DS_Store files +.DS_Store + +# Proguard README +proguard-project.txt +tests/proguard-project.txt + +# Android Studio and Gradle specific entries +.gradle +.idea/* +!.idea/codeStyles/ +build +/gradle.properties +.attach_pid* +fastlane/Fastfile +*.hprof + +# fastlane specific +**/fastlane/report.xml + +# deliver temporary files +**/fastlane/Preview.html + +# snapshot generated screenshots +**/fastlane/screenshots + +# scan temporary files +**/fastlane/test_output +/fastlane/vendor/ +/.bundle/ +/fastlane/.bundle + +# python +**/__pycache__/ +/gradle/verification-keyring.gpg diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..23a6687 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,205 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.pullapprove.yml b/.pullapprove.yml new file mode 100644 index 0000000..02b4872 --- /dev/null +++ b/.pullapprove.yml @@ -0,0 +1,50 @@ +version: 2 +# SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only +# General settings to apply +always_pending: + title_regex: '(WIP|wip)' + labels: + - 1. developing + # custom message that will be used for the GitHub status + explanation: 'This PR is a work in progress...' + +# Group settings to apply to all groups by default, optionally being overridden later +group_defaults: + author_approval: + ignored: true + approve_by_comment: + enabled: true + approve_regex: '^(Approved|:shipit:|:\+1:|LGTM|Merge)' + reject_regex: '^(Rejected|:-1:)' + reset_on_push: + enabled: false + reset_on_reopened: + enabled: true + conditions: + labels: + exclude: + - dependencies + +groups: + code-review: + required: 1 + reject_value: -99 + users: + - AndyScherzinger + - tobiasKaminsky + - mario + - przybylski + - ardevd + + design-review: + conditions: + labels: + - design + reset_on_push: + enabled: false + required: 1 + reject_value: -99 + users: + - jancborchardt + - eppfel diff --git a/.tx/config b/.tx/config new file mode 100644 index 0000000..f0d4139 --- /dev/null +++ b/.tx/config @@ -0,0 +1,10 @@ +[main] +host = https://www.transifex.com + +[o:nextcloud:p:nextcloud:r:android] +file_filter = app/src/main/res/values-/strings.xml +source_file = app/src/main/res/values/strings.xml +source_lang = en +type = ANDROID +lang_map = en_SG: en-rSG, es_PA: es-rPA, mr_IN: mr-rIN, cs_CZ: cs-rCZ, vi_VN: vi-rVN, fr_MC: fr-rMC, it_CH: it-rCH, ta_LK: ta-rLK, kk_KZ: kk-rKZ, mn_CN: mn-rCN, mt_MT: mt-rMT, sma_SE: sma-rSE, si_LK: si-rLK, pl_PL: pl-rPL, de_AT: de-rAT, ii_CN: ii-rCN, hi_IN: hi-rIN, ar_BH: ar-rBH, ar_JO: ar-rJO, es_NI: es-rNI, quz_BO: quz-rBO, sr_CS: sr-rCS, es_CO: es-rCO, es_GT: es-rGT, ml_IN: ml-rIN, rm_CH: rm-rCH, zh_CN.GB2312: zh-rBG, hr_BA: hr-rBA, se_FI: se-rFI, tn_ZA: tn-rZA, tzm_DZ: tzm-rDZ, en_ZA: en-rZA, es_419: b+es+419, en_IN: en-rIN, my_MM: my, se_NO: se-rNO, am_ET: am-rET, arn_CL: arn-rCL, en_MY: en-rMY, es_HN: es-rHN, es_UY: es-rUY, en_AU: en-rAU, id: in, ku_IQ: ku-rIQ, pt_BR: pt-rBR, xh_ZA: xh-rZA, co_FR: co-rFR, en_BZ: en-rBZ, ha_NG: ha-rNG, or_IN: or-rIN, dsb_DE: dsb-rDE, fo_FO: fo-rFO, fr_CA: fr-rCA, ky_KG: ky-rKG, ar_LB: ar-rLB, es_AR: es-rAR, is_IS: is-rIS, ar_KW: ar-rKW, en_GB: b+en+001, fy_NL: fy-rNL, ar_QA: ar-rQA, hy_AM: hy-rAM, mn_MN: mn-rMN, nl_BE: nl-rBE, ar_OM: ar-rOM, as_IN: as-rIN, cy_GB: cy-rGB, he: iw, it_IT: it-rIT, nso_ZA: nso-rZA, ba_RU: ba-rRU, wo_SN: wo-rSN, lb_LU: lb-rLU, quz_EC: quz-rEC, uz_UZ: uz-rUZ, zh_TW: zh-rTW, ar_MA: ar-rMA, es_CL: es-rCL, es_VE: es-rVE, da_DK: da-rDK, et_EE: et-rEE, af_ZA: af-rZA, en@pirate: en-rpirate, ga_IE: ga-rIE, kok_IN: kok-rIN, ur_PK: ur-rPK, tg_TJ: tg-rTJ, ne_NP: ne-rNP, es_CR: es-rCR, fil_PH: fil-rPH, fr_CH: fr-rCH, gl_ES: gl-rES, se_SE: se-rSE, sr_BA: sr-rBA, es_DO: es-rDO, ms_MY: ms-rMY, oc_FR: oc-rFR, syr_SY: syr-rSY, ug_CN: ug-rCN, en_CA: en-rCA, en_JM: en-rJM, ko_KR: ko-rKR, be_BY: be-rBY, zh_HK: zh-rHK, nb_NO: nb-rNO, fi_FI: fi-rFI, fr_FR: fr-rFR, ar_SA: ar-rSA, az_AZ: az-rAZ, he_IL: he-rIL, zh_CN: zh-rCN, bn_BD: bn-rBD, el_GR: el-rGR, en_PH: en-rPH, sr@latin: sr-rSP, br_FR: br-rFR, ta_IN: ta-rIN, hu_HU: hu-rHU, lt_LT: lt-rLT, ar_AE: ar-rAE, en_ZW: en-rZW, ar_TN: ar-rTN, ka_GE: ka-rGE, en_TT: en-rTT, mi_NZ: mi-rNZ, zu_ZA: zu-rZA, fa_IR: fa-rIR, fr_LU: fr-rLU, lo_LA: lo-rLA, ms_BN: ms-rBN, rw_RW: rw-rRW, sl_SI: sl-rSI, tt_RU: tt-rRU, de_LI: de-rLI, es_EC: es-rEC, ps_AF: ps-rAF, id_ID: id-rID, smn_FI: smn-rFI, bg_BG: bg-rBG, lv_LV: lv-rLV, te_IN: te-rIN, iu_CA: iu-rCA, sms_FI: sms-rFI, es_PE: es-rPE, gd_GB: gd-rGB, hr_HR: hr-rHR, moh_CA: moh-rCA, smj_SE: smj-rSE, ar_LY: ar-rLY, de_LU: de-rLU, es_BO: es-rBO, sq_AL: sq-rAL, ar_SY: ar-rSY, tr_TR: tr-rTR, sr_RS: sr-rRS, sv_SE: sv-rSE, kl_GL: kl-rGL, quz_PE: quz-rPE, de_DE: de-rDE, sv_FI: sv-rFI, tk_TM: tk-rTM, bo_CN: bo-rCN, gsw_FR: gsw-rFR, pt_PT: pt-rPT, dv_MV: dv-rMV, uk_UA: uk-rUA, ar_YE: ar-rYE, zh_SG: zh-rSG, sw_KE: sw-rKE, en_IE: en-rIE, en_US: en-rUS, es_SV: es-rSV, qut_GT: qut-rGT, th_TH: th-rTH, ar_DZ: ar-rDZ, gu_IN: gu-rIN, kn_IN: kn-rIN, mk_MK: mk-rMK, es_MX: es-rMX, ig_NG: ig-rNG, smj_NO: smj-rNO, bn_IN: bn-rIN, de_CH: de-rCH, sk_SK: sk-rSK, es_PR: es-rPR, yo_NG: yo-rNG, sma_NO: sma-rNO, sa_IN: sa-rIN, en_NZ: en-rNZ, ja_JP: ja-rJP, pa_IN: pa-rIN, es_PY: es-rPY, nn_NO: nn-rNO, ar_EG: ar-rEG, bs_BA: bs-rBA, eu_ES: eu-rES, fr_BE: fr-rBE, km_KH: km-rKH, ru_RU: ru-rRU, sah_RU: sah-rRU, ca_ES: ca-rES, sr_ME: sr-rME, ro_RO: ro-rRO, prs_AF: prs-rAF, zh_MO: zh-rMO, es_ES: es-rES, hsb_DE: hsb-rDE, nl_NL: nl-rNL, ar_IQ: ar-rIQ + diff --git a/.tx/config.license b/.tx/config.license new file mode 100644 index 0000000..39d0c45 --- /dev/null +++ b/.tx/config.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors +SPDX-FileCopyrightText: 2012 Bartosz Przybylski +SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 97df73b..3374f92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,157 @@ +## 3.33.0 (September 10, 2025) + +- Migrate to Glide 4 +- Performance improvements +- Fix gallery image scaling +- Bugfixes + +Minimum: NC 18 Server, Android 8.1 Nougat + +For a full list, please see https://github.com/nextcloud/android/milestone/112 + +## 3.32.3 (August 21, 2025) + +- Bugfixes + +Minimum: NC 18 Server, Android 8.0 Nougat + +For a full list, please see https://github.com/nextcloud/android/milestone/114 + +## 3.32.2 (July 18, 2025) + +- Resolved image blurriness issue. +- Fixed crash occurring in the conflict resolution dialog. +- Addressed crash in the upload finish receiver event handler. + +Minimum: NC 18 Server, Android 8.0 Nougat + +For a full list, please see https://github.com/nextcloud/android/milestone/115 + +## 3.32.1 (July 14, 2025) + +- Bug fixes. + +Minimum: NC 18 Server, Android 8.0 Nougat + +For a full list, please see https://github.com/nextcloud/android/milestone/113 + +## 3.32.0 (July 2, 2025) + +- Minimum supported Android version is 8.0. +- Scrolling performance has been increased in the media tab. +- Multi-select feature added to the media tab. +- Custom share permissions have been added. +- Bug fixes. + +Minimum: NC 18 Server, Android 8.0 Nougat + +For a full list, please see https://github.com/nextcloud/android/milestone/107 + +## 3.31.4 (June 3, 2025) + +- Add missing auto migration + +Minimum: NC 18 Server, Android 7.1 Nougat + +For a full list, please see https://github.com/nextcloud/android/milestone/110 + +## 3.31.3 (May 28, 2025) + +- fix simple sign up +- bugfixes +- update translations + +Minimum: NC 18 Server, Android 7.1 Nougat + +For a full list, please see https://github.com/nextcloud/android/milestone/110 + +## 3.31.2 (May 20, 2025) + +- bring back MANAGE_EXTERNAL_STORAGE permission + +Minimum: NC 18 Server, Android 7.1 Nougat + +For a full list, please see https://github.com/nextcloud/android/milestone/108 + +## 3.31.1 (April 3, 2025) + +- Various bug fixes and performance enhancements + +Minimum: NC 18 Server, Android 7.1 Nougat + +For a full list, please see https://github.com/nextcloud/android/milestone/108 + +## 3.31.0 (February 25, 2025) + +- New share layout +- Various bug fixes and performance enhancements + +Minimum: NC 18 Server, Android 7.1 Nougat + +For a full list, please see https://github.com/nextcloud/android/milestone/100 + +## 3.30.7 (January 6, 2025) + +- Fix crash of auto upload settings + +Minimum: NC 16 Server, Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/android/milestone/104 + +## 3.30.3 (October 22, 2024) + +- Bugfix for two way sync: sync only on wifi + +## 3.30.2 (October 21, 2024) + +- Bugfix for two way sync. Please check listed folders in settings -> internal two way sync + +Minimum: NC 16 Server, Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/android/milestone/99 + +## 3.30.1 (October 11, 2024) + +- Bugfixes + +Minimum: NC 16 Server, Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/android/milestone/99 + +## 3.29.1 (June 27, 2024) + +- Bugfixes + +Minimum: NC 16 Server, Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/android/milestone/93 + + + +## 3.29.0 (April 24, 2024) + +- NC Assistant +- Client certificates +- Personal files view +- REUSE compliance +- Bugfixes + +Minimum: NC 16 Server, Android 7.0 Nougat + +For a full list, please see https://github.com/nextcloud/android/milestone/89 + ## 3.28.2 (April 4th, 2024) - Bugfixes - Minimum: NC 16 Server, Android 7.0 Nougat For a full list, please see https://github.com/nextcloud/android/milestone/90 - - ## 3.28.1 (March 25th, 2024) - Bugfixes @@ -25,7 +165,6 @@ For a full list, please see https://github.com/nextcloud/android/milestone/90 - E2E sharing - Bugfixes - Minimum: NC 16 Server, Android 7.0 Nougat For a full list, please see https://github.com/nextcloud/android/milestone/88 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index aedac86..ae8a32d 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,6 +1,6 @@ In the Nextcloud community, participants from all over the world come together to create Free Software for a free internet. This is made possible by the support, hard work and enthusiasm of thousands of people, including those who create and use Nextcloud software. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2f0ad39..5fab39e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # [Nextcloud](https://nextcloud.com) Android app @@ -191,17 +191,17 @@ Source code of app: * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2024 Your Name - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ ``` XML (layout) file: ```xml ``` diff --git a/Gemfile.lock b/Gemfile.lock index 234c43e..764030f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,42 +5,44 @@ GEM base64 nkf rexml - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) artifactory (3.0.17) atomos (0.1.3) - aws-eventstream (1.3.0) - aws-partitions (1.913.0) - aws-sdk-core (3.191.6) + aws-eventstream (1.4.0) + aws-partitions (1.1121.0) + aws-sdk-core (3.226.1) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.78.0) - aws-sdk-core (~> 3, >= 3.191.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.146.1) - aws-sdk-core (~> 3, >= 3.191.0) + logger + aws-sdk-kms (1.106.0) + aws-sdk-core (~> 3, >= 3.225.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.191.0) + aws-sdk-core (~> 3, >= 3.225.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) - base64 (0.2.0) - cgi (0.4.1) + base64 (0.3.0) + cgi (0.4.2) claide (1.1.0) colored (1.2) colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) declarative (0.0.20) - digest-crc (0.6.5) + digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.110.0) - faraday (1.10.3) + excon (0.112.0) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -56,20 +58,20 @@ GEM faraday (>= 0.8.0) http-cookie (~> 1.0.0) faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) + faraday-em_synchrony (1.0.1) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) - faraday-net_http (1.0.1) + faraday-multipart (1.1.1) + multipart-post (~> 2.0) + faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) - faraday_middleware (1.2.0) + faraday_middleware (1.2.1) faraday (~> 1.0) - fastimage (2.3.1) - fastlane (2.220.0) + fastimage (2.4.0) + fastlane (2.228.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -85,6 +87,7 @@ GEM faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) @@ -108,10 +111,12 @@ GEM tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) - xcpretty (~> 0.3.0) + xcpretty (~> 0.4.1) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) - fastlane-plugin-huawei_appgallery_connect (1.0.28) + fastlane-plugin-huawei_appgallery_connect (1.0.31) cgi + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) gh_inspector (1.1.3) google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) @@ -129,12 +134,12 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.7.0) + google-cloud-core (1.8.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.4.0) + google-cloud-errors (1.5.0) google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) @@ -150,36 +155,39 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.5) + http-cookie (1.0.8) domain_name (~> 0.5) - httpclient (2.8.3) + httpclient (2.9.0) + mutex_m jmespath (1.6.2) - json (2.7.2) - jwt (2.8.1) + json (2.12.2) + jwt (2.10.1) base64 - mini_magick (4.12.0) + logger (1.7.0) + mini_magick (4.13.2) mini_mime (1.1.5) multi_json (1.15.0) - multipart-post (2.4.0) - nanaimo (0.3.0) - naturally (2.2.1) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + naturally (2.3.0) nkf (0.2.0) - optparse (0.4.0) + optparse (0.6.0) os (1.1.4) - plist (3.7.1) - public_suffix (5.0.5) - rake (13.2.1) + plist (3.7.2) + public_suffix (6.0.2) + rake (13.3.0) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.6) - rouge (2.0.7) + rexml (3.4.1) + rouge (3.28.0) ruby2_keywords (0.0.5) - rubyzip (2.3.2) + rubyzip (2.4.1) security (0.1.5) - signet (0.19.0) + signet (0.20.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) @@ -187,6 +195,7 @@ GEM simctl (1.6.10) CFPropertyList naturally + sysrandom (1.0.5) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -196,17 +205,17 @@ GEM tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.24.0) + xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (~> 3.2.4) - xcpretty (0.3.0) - rouge (~> 2.0.7) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.1) + rouge (~> 3.28.0) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) diff --git a/LICENSES/LicenseRef-XTrademarks.txt b/LICENSES/LicenseRef-XTrademarks.txt new file mode 100644 index 0000000..46b6983 --- /dev/null +++ b/LICENSES/LicenseRef-XTrademarks.txt @@ -0,0 +1,49 @@ +Trademark policy +April 2023 + + +You may not violate others’ intellectual property rights, including copyright and trademark. + +A trademark is a word, logo, phrase, or device that distinguishes a trademark holder’s good or service in the marketplace. Trademark law may prevent others from using a trademark in an unauthorized or confusing manner. + + +What is in violation of this policy? + +Using another’s trademark in a way that may mislead or confuse people about your affiliation may be a violation of our trademark policy. + + +What is not a violation of this policy? + +Referencing another’s trademark is not automatically a violation of X's trademark policy. Examples of non-violations include: + +* using a trademark in a way that is outside the scope of the trademark registration e.g., in a different territory, or a different class of goods or services than that identified in the registration; and +* using a trademark in a nominative or other fair use manner. For more information, see our Misleading and deceptive identities policy (https://help.twitter.com/en/rules-and-policies/twitter-impersonation-and-deceptive-identities-policy.html). + + +Who can report violations of this policy? + +X only investigates requests that are submitted by the trademark holder or their authorized representative e.g., a legal representative or other representative for a brand. + + +How can I report violations of this policy? + +You can submit a trademark report through our trademark report form (https://help.twitter.com/forms/trademark). Please provide all the information requested in the form. If you submit an incomplete report, we’ll need to follow up about the missing information. Please note that this will result in a delay in processing your report. + +Note: We may provide the account holder with your name and other information included in the copy of the report. + + +What happens if you violate this policy? + +If we determine that you violated our trademark policy, we may suspend your account. Depending on the type of violation, we may give you an opportunity to comply with our policies. In other instances, an account may be permanently suspended upon first review. If you believe that your account was suspended in error, you can submit an appeal (https://help.twitter.com/forms/general?subtopic=suspended). + + +Additional resources + +Learn more about our range of enforcement options (https://help.twitter.com/rules-and-policies/enforcement-options) and our approach to policy development and enforcement (https://help.twitter.com/rules-and-policies/enforcement-philosophy). + + +Legal disclaimer + +By using the X trademarks and resources on this site, you agree to follow the X Trademark Guidelines in our Brand Guidelines — as well as our Terms of Service and all other X rules and policies. If you have any questions, contact us at trademarks@x.com. + +A copy can be found at https://about.x.com/en/who-we-are/brand-toolkit and https://help.twitter.com/en/rules-and-policies/x-trademark-policy diff --git a/README.md b/README.md index 94c71a3..b4d33da 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ # [Nextcloud](https://nextcloud.com) Android app :iphone: -[![REUSE status](https://api.reuse.software/badge/github.com/nextcloud/android)](https://api.reuse.software/info/github.com/nextcloud/android) [![Build Status](https://drone.nextcloud.com/api/badges/nextcloud/android/status.svg)](https://drone.nextcloud.com/nextcloud/android) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/80401cb343854343b4d94acbfb72d3ec)](https://www.codacy.com/app/Nextcloud/android?utm_source=github.com\&utm_medium=referral\&utm_content=nextcloud/android\&utm_campaign=Badge_Grade) [![Releases](https://img.shields.io/github/release/nextcloud/android.svg)](https://github.com/nextcloud/android/releases/latest) +[![REUSE status](https://api.reuse.software/badge/github.com/nextcloud/android)](https://api.reuse.software/info/github.com/nextcloud/android) [![Build Status](https://drone.nextcloud.com/api/badges/nextcloud/android/status.svg)](https://drone.nextcloud.com/nextcloud/android) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/fb4cf26336774ee3a5c9adfe829c41aa)](https://app.codacy.com/gh/nextcloud/android/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) [![Releases](https://img.shields.io/github/release/nextcloud/android.svg)](https://github.com/nextcloud/android/releases/latest) -[](https://play.google.com/store/apps/details?id=com.nextcloud.client) -[](https://f-droid.org/packages/com.nextcloud.client/) +Signing certificate fingerprint to [verify](https://developer.android.com/studio/command-line/apksigner#usage-verify) the APK: +- APK with "gplay" name, found [here](https://github.com/nextcloud/android/releases) or distributed via Google Play Store +- APK with "nextcloud", found [here](https://github.com/nextcloud/android/releases) +- not suitable for Fdroid downloads, as Fdroid is signing it on their own +``` +SHA-256: fb009522f65e25802261b67b10a45fd70e610031976f40b28a649e152ded0373 +SHA-1: 74aa1702e714941be481e1f7ce4a8f779c19dcea +``` **The Android client for [Nextcloud](https://nextcloud.com). Easily work with your data on your Nextcloud.** diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000..59e6e3b --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +version = 1 +SPDX-PackageName = "Nextcloud Android" +SPDX-PackageSupplier = "Nextcloud Android team " +SPDX-PackageDownloadLocation = "https://github.com/nextcloud/android" + +[[annotations]] +path = "gradle/wrapper/gradle-wrapper.jar" +precedence = "aggregate" +SPDX-FileCopyrightText = "2015-2021 the original authors" +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = ["user_manual/images/android-1.png", "user_manual/images/android-2.png", "user_manual/images/android-3.png", "user_manual/images/android-4.png", "user_manual/images/android-10.png", "user_manual/images/davdroid-1-button-in-nextcloud-app.png", "user_manual/images/davdroid-2-install-davdroid.png", "user_manual/images/davdroid-3-enter-password.png", "user_manual/images/davdroid-4-specify-owner-email.png"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2016-2024 Nextcloud GmbH and Nextcloud contributors" +SPDX-License-Identifier = "AGPL-3.0-or-later" + +[[annotations]] +path = ["user_manual/conf.py", "user_manual/android_app.rst", "user_manual/index.rst", "user_manual/conf.py", "user_manual/Makefile"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2015-2016 ownCloud Inc., 2016-2024 Nextcloud GmbH" +SPDX-License-Identifier = "GPL-2.0-only" + +[[annotations]] +path = ["user_manual/images/android-11.png", "user_manual/images/android-12.png", "user_manual/images/android-13.png", "user_manual/images/android-14.png", "user_manual/images/android-15.png", "user_manual/images/android-5.png", "user_manual/images/android-6.png", "user_manual/images/android-8.png", "user_manual/images/android-9.png"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2015-2016 ownCloud Inc." +SPDX-License-Identifier = "GPL-2.0-only" + +[[annotations]] +path = ["app/src/**/res/mipmap-**dpi/ic_launcher.png", "app/src/**/ic_launcher-web.png", "src/generic/fastlane/metadata/android/en-US/images/icon.png", "src/versionDev/fastlane/metadata/android/en-US/images/icon.png", "app/src/main/ic_launcher-web-round.png"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2017-2024 Nextcloud GmbH " +SPDX-License-Identifier = "LicenseRef-NextcloudTrademarks" + +[[annotations]] +path = [".idea/**", "app/schemas/com.nextcloud.client.database.NextcloudDatabase/**.json", "app/screenshots/generic/debug/**.png", "app/src/main/res/values-**/strings.xml", "src/**/fastlane/metadata/android/**/**.txt", "src/versionDev/fastlane/metadata/android/**/changelogs/**.txt", "app/src/androidTest/assets/**", "app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker", "app/src/**/google-services.json", "app/src/main/res/drawable-**dpi/checker_16_16.png", "app/src/main/res/raw/encryption_key_words.txt", "app/src/main/resources/ical4j.properties", "app/src/main/res/drawable-**dpi/apk.png", "app/src/main/res/drawable-**dpi/fdroid.png", "app/src/main/res/drawable-**dpi/playstore.png", "app/src/main/res/drawable-**dpi/background.png", "app/src/main/res/drawable-**dpi/background_nc18.png"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2016-2024 Nextcloud GmbH and Nextcloud contributors" +SPDX-License-Identifier = "AGPL-3.0-or-later" diff --git a/Readme-AR.md b/Readme-AR.md new file mode 100644 index 0000000..98735aa --- /dev/null +++ b/Readme-AR.md @@ -0,0 +1,120 @@ + +# تطبيق [Nextcloud](https://nextcloud.com)لأجهزة أندرويد 📱 + +[![حالة REUSE](https://api.reuse.software/badge/github.com/nextcloud/android)](https://api.reuse.software/info/github.com/nextcloud/android) +[![حالة البناء](https://drone.nextcloud.com/api/badges/nextcloud/android/status.svg)](https://drone.nextcloud.com/nextcloud/android) +[![تقييم Codacy](https://app.codacy.com/project/badge/Grade/fb4cf26336774ee3a5c9adfe829c41aa)](https://app.codacy.com/gh/nextcloud/android/dashboard) +[![الإصدارات](https://img.shields.io/github/release/nextcloud/android.svg)](https://github.com/nextcloud/android/releases/latest) + +[تحميل من Google Play](https://play.google.com/store/apps/details?id=com.nextcloud.client) +[احصل عليه من F-Droid](https://f-droid.org/packages/com.nextcloud.client/) + +## التحقق من توقيع التطبيق 🔐 + +للتأكد من صحة ملف APK: + +- ملف APK باسم "gplay" متوفر [هنا](https://github.com/nextcloud/android/releases) أو عبر متجر Google Play +- ملف APK باسم "nextcloud" متوفر [هنا](https://github.com/nextcloud/android/releases) +- غير مناسب لتحميلات F-Droid، لأن F-Droid يقوم بتوقيعه بنفسه + +```plaintext +SHA-256: fb009522f65e25802261b67b10a45fd70e610031976f40b28a649e152ded0373 +SHA-1: 74aa1702e714941be481e1f7ce4a8f779c19dcea +``` + +**تطبيق Nextcloud لأندرويد يتيح لك إدارة بياناتك بسهولة على خادم Nextcloud الخاص بك.** + +## الحصول على الدعم 🆘 + +إذا واجهت مشكلة أو لديك سؤال، يمكنك زيارة [منتدى الدعم](https://help.nextcloud.com/c/clients/android). +إذا اكتشفت خطأ أو لديك اقتراح لتحسين التطبيق، يمكنك [فتح قضية جديدة على GitHub](https://github.com/nextcloud/android/issues). + +إذا لم تكن متأكدًا ما إذا كانت المشكلة ناتجة عن التطبيق أو الإعدادات أو الخادم، فابدأ بالسؤال في المنتدى، ثم عد إلى GitHub إذا لزم الأمر. + +> ملاحظة: هذا المستودع خاص بتطبيق أندرويد فقط. إذا كانت المشكلة في الخادم، يرجى التواصل مع [فريق خادم Nextcloud](https://github.com/nextcloud/server). + +## كيف تساهم في المشروع 🚀 + +هناك العديد من الطرق للمساهمة، سواء كنت مبرمجًا أو لا: + +- مساعدة المستخدمين في المنتدى: https://help.nextcloud.com +- ترجمة التطبيق عبر [Transifex](https://app.transifex.com/nextcloud/nextcloud/android/) +- الإبلاغ عن المشاكل أو تقديم اقتراحات عبر [GitHub Issues](https://github.com/nextcloud/android/issues/new/choose) +- تنفيذ إصلاحات أو تحسينات عبر Pull Requests +- مراجعة [طلبات الدمج](https://github.com/nextcloud/android/pulls) +- اختبار النسخ التجريبية أو اليومية أو المرشحة للإصدار +- تحسين [التوثيق](https://github.com/nextcloud/documentation/) +- اختبار الميزات الأساسية في آخر إصدار مستقر +- تعلم كيفية جمع سجلات الأخطاء (logcat) لتقديم تقارير دقيقة + +## إرشادات المساهمة والترخيص 📜 + +- الترخيص: [GPLv2](https://github.com/nextcloud/android/blob/master/LICENSE.txt) +- جميع المساهمات بعد 16 يونيو 2016 تعتبر مرخصة تحت AGPLv3 أو أي إصدار لاحق +- لا حاجة لتوقيع اتفاقية مساهم (CLA) +- يُفضل إضافة السطر التالي في رأس الملف عند إجراء تغييرات كبيرة: + +```plaintext +SPDX-FileCopyrightText: <السنة> <اسمك> <بريدك الإلكتروني> +``` + +يرجى قراءة [مدونة السلوك](https://nextcloud.com/community/code-of-conduct/) لضمان بيئة تعاون إيجابية. +راجع أيضًا [إرشادات المساهمة](https://github.com/nextcloud/android/blob/master/CONTRIBUTING.md). + +## ابدأ بالمساهمة 🔧 + +- اقرأ [SETUP.md](https://github.com/nextcloud/android/blob/master/SETUP.md) و[CONTRIBUTING.md](https://github.com/nextcloud/android/blob/master/CONTRIBUTING.md) +- قم بعمل fork للمستودع وابدأ بإرسال Pull Requests إلى فرع master +- يمكنك البدء بمراجعة [طلبات الدمج](https://github.com/nextcloud/android/pulls) أو العمل على [القضايا المبتدئة](https://github.com/nextcloud/android/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) + +## جمع سجلات الأخطاء (logcat) 🔍 + +### على لينكس: + +- فعّل USB-Debugging على هاتفك +- افتح الطرفية وأدخل: + +```bash +adb logcat --pid=$(adb shell pidof -s 'com.nextcloud.client') > logcatOutput.txt +``` + +> تأكد من تثبيت [adb](https://developer.android.com/studio/releases/platform-tools.html) + +### على ويندوز: + +- حمّل [Minimal ADB and Fastboot](https://forum.xda-developers.com/t/tool-minimal-adb-and-fastboot-2-9-18.2317790/#post-42407269) +- فعّل USB-Debugging +- افتح البرنامج وأدخل: + +```bash +adb shell pidof -s 'com.nextcloud.client' +``` + +- استخدم الناتج كـ `` في الأمر التالي: + +```bash +adb logcat --pid= > "%USERPROFILE%\Downloads\logcatOutput.txt" +``` + +### على الجهاز (مع صلاحيات root): + +```bash +su +logcat -d --pid $(pidof -s com.nextcloud.client) -f /sdcard/logcatOutput.txt +``` + +أو استخدم تطبيقات مثل [CatLog](https://play.google.com/store/apps/details?id=com.nolanlawson.logcat) أو [aLogcat](https://play.google.com/store/apps/details?id=org.jtb.alogcat) + +## النسخة التطويرية 🛠️ + +- [تحميل مباشر للـ APK](https://download.nextcloud.com/android/dev/latest.apk) +- [F-Droid النسخة التجريبية](https://f-droid.org/en/packages/com.nextcloud.android.beta/) + +## المشاكل المعروفة والأسئلة الشائعة + +### الإشعارات الفورية لا تعمل في نسخ F-Droid + +بسبب اعتمادها على خدمات Google Play، لا تعمل الإشعارات الفورية في نسخ F-Droid حاليًا. diff --git a/SECURITY.md b/SECURITY.md index c6aea0b..b0adf57 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,28 +1,71 @@ # Security Policy -## Supported Versions +# 💡 TLDR: Report issues at [hackerone.com/nextcloud](https://hackerone.com/nextcloud) + +# Security Policy + +[Security](https://nextcloud.com/security/) is very important to us. + +If you believe you have found a security vulnerability that meets our definition of a security +vulnerability, please report is as described below. + +## Context + +Please review our [threat model and accepted risks](https://nextcloud.com/security/threat-model) to learn what +is currently considered a security vulnerability versus expected behavior. And review what is considered +[in scope or bounty eligible](https://hackerone.com/nextcloud/policy_scopes). -Only the latest version is supported. We release every second month a feature release (currently 3.x) and inbetween a bug fix release (3.x.y). ## Reporting a Vulnerability -Security is very important to us. If you have discovered a security issue with Nextcloud, -please read our responsible disclosure guidelines and contact us at [hackerone.com/nextcloud](https://hackerone.com/nextcloud). +**⚠️ Please do _not_ report security vulnerabilities through public GitHub issues.** + +If you have discovered a security matter with Nextcloud, please read our +[responsible disclosure guidelines](https://nextcloud.com/security/) and contact us at +[hackerone.com/nextcloud](https://hackerone.com/nextcloud). + Your report should include: - Product version - A vulnerability description - Reproduction steps +- Any other details you think are likely to be important -A member of the security team will confirm the vulnerability, determine its impact, and develop a fix. -The fix will be applied to the master branch, tested, and packaged in the next bug fix release. +### What to Expect + +You should receive an initial acknowledgement within 24 hours in most cases. + +A member of the security team will confirm the vulnerability, determine its impact, follow-up with any questions, +and coordinate the fix and publication. + +The fix will be applied to all applicable and still supported stable branches, tested, and packaged in the next security release. The vulnerability will be publicly announced after the release. Finally, your name will be added -to the [hall of fame](https://hackerone.com/nextcloud/thanks) as a thank you from the entire Nextcloud community. Note our -[threat model](https://nextcloud.com/security/threat-model) to know what is expected behavior. +to the [hall of fame](https://hackerone.com/nextcloud/thanks) as a thank you from the entire Nextcloud +community. +If the vulnerability involves an app that is not maintained by Nextcloud (i.e. hosted by the +Nextcloud project but community maintained, or hosted elsewhere), the security team will try to coordinate with the +current maintainer and help to get the issue fixed in similar fashion. -Please visit https://nextcloud.com/security/ for further information about security. +### Bug Bounties + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Details +on past bounty ranges can be found at [hackerone.com/nextcloud](https://hackerone.com/nextcloud). + +## Existing Security Advisories + +Published security advisories for the Nextcloud Server, Clients and Apps can be viewed at +[https://github.com/nextcloud/security-advisories/security/advisories](https://github.com/nextcloud/security-advisories/security/advisories). + +## Supported Versions + +Only the latest version is supported. We release every second month a feature release (currently 3.x) and inbetween a bug fix release (3.x.y). + +## Additional Information + +Please visit [https://nextcloud.com/security/](https://nextcloud.com/security/) for further information about Nextcloud security. +Please visit [https://nextcloud.com/security/threat-model](https://nextcloud.com/security/threat-model) for our threat model and accepted risks. diff --git a/SETUP.md b/SETUP.md index 36f8362..d1a6e6a 100644 --- a/SETUP.md +++ b/SETUP.md @@ -1,6 +1,6 @@ These instructions will help you to set up your development environment, get the source code of the Nextcloud for Android app and build it by yourself. If you want to help developing the app take a look to the [contribution guidelines][0]. diff --git a/app/.gitignore b/app/.gitignore index 828b797..e666d0b 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,3 +1,3 @@ # SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later +# SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only /build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index d2b1fd6..ba7c735 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,11 +2,11 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2024 Tobias Kaminsky * SPDX-FileCopyrightText: 2024 Andy Scherzinger * SPDX-FileCopyrightText: 2022 Álvaro Brey Vilas - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ import com.github.spotbugs.snom.Confidence import com.github.spotbugs.snom.Effort @@ -16,11 +16,11 @@ import org.gradle.internal.jvm.Jvm buildscript { dependencies { classpath "com.android.tools.build:gradle:$androidPluginVersion" - classpath 'com.github.spotbugs.snom:spotbugs-gradle-plugin:6.0.12' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.6" - classpath "commons-httpclient:commons-httpclient:3.1@jar" // remove after entire switch to lib v2 - classpath 'com.karumi:shot:6.1.0' + classpath libs.spotbugs.gradle.plugin + classpath libs.kotlin.gradle.plugin + classpath libs.detekt.gradle.plugin + classpath libs.commons.httpclient.commons.httpclient // remove after entire switch to lib v2 + classpath libs.shot classpath "org.jacoco:org.jacoco.core:$jacoco_version" classpath "org.jacoco:org.jacoco.report:$jacoco_version" classpath "org.jacoco:org.jacoco.agent:$jacoco_version" @@ -28,40 +28,35 @@ buildscript { } plugins { - id "com.diffplug.spotless" version "6.20.0" - id 'com.google.devtools.ksp' version '1.9.23-1.0.20' apply false + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.spotless) + alias(libs.plugins.kapt) + alias(libs.plugins.ksp) apply false } -apply plugin: 'com.android.application' +apply plugin: "com.android.application" -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' -apply plugin: 'kotlin-parcelize' -apply plugin: 'checkstyle' -apply plugin: 'pmd' +apply plugin: "kotlin-android" +apply plugin: "kotlin-parcelize" +apply plugin: "checkstyle" +apply plugin: "pmd" apply from: "$rootProject.projectDir/jacoco.gradle" -apply plugin: 'com.github.spotbugs' -apply plugin: 'io.gitlab.arturbosch.detekt' +apply plugin: "com.github.spotbugs" +apply plugin: "io.gitlab.arturbosch.detekt" // needed to make renovate run without shot, as shot requires Android SDK // https://github.com/pedrovgs/Shot/issues/300 if (shotTest) { - apply plugin: 'shot' + apply plugin: "shot" } -apply plugin: 'com.google.devtools.ksp' +apply plugin: "com.google.devtools.ksp" println "Gradle uses Java ${Jvm.current()}" configurations { configureEach { - exclude group: 'org.jetbrains', module: 'annotations-java5' // via prism4j, already using annotations explicitly - - // check for updates every build - resolutionStrategy { - cacheChangingModulesFor 0, 'seconds' - exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug" - } + exclude group: "org.jetbrains", module: "annotations-java5" // via prism4j, already using annotations explicitly } } @@ -72,39 +67,59 @@ configurations.configureEach { useVersion(checkerVersion) because("https://github.com/google/ExoPlayer/issues/10007") } + + if (requested.group == "commons-logging" && requested.name == "commons-logging") { + useTarget("org.slf4j:jcl-over-slf4j:1.7.4") + } } } // semantic versioning for version code def versionMajor = 3 -def versionMinor = 29 -def versionPatch = 3 +def versionMinor = 34 +def versionPatch = 0 def versionBuild = 90 // 0-50=Alpha / 51-98=RC / 90-99=stable + def ndkEnv = new HashMap() file("$project.rootDir/ndk.env").readLines().each() { - def (key, value) = it.tokenize('=') + def (key, value) = it.tokenize("=") ndkEnv.put(key, value) } -def perfAnalysis = project.hasProperty('perfAnalysis') +def perfAnalysis = project.hasProperty("perfAnalysis") + +def getConfigProperties() { + def props = new Properties() + def file = rootProject.file(".gradle/config.properties") + if (file.exists()) { + props.load(new FileInputStream(file)) + } + return props +} + +def configProps = getConfigProperties() android { // install this NDK version and Cmake to produce smaller APKs. Build will still work if not installed - ndkVersion "${ndkEnv.get("NDK_VERSION")}" + ndkVersion = "${ndkEnv.get("NDK_VERSION")}" - namespace 'com.owncloud.android' - testNamespace "${namespace}.test" + namespace = "com.owncloud.android" + testNamespace = "${namespace}.test" + androidResources { + generateLocaleConfig = true + } defaultConfig { - minSdkVersion 24 - targetSdkVersion 34 - compileSdk 34 + applicationId = "com.nextcloud.client" + minSdk = 27 + targetSdk = 35 + compileSdk = 35 - buildConfigField 'boolean', 'CI', ciBuild.toString() - buildConfigField 'boolean', 'RUNTIME_PERF_ANALYSIS', perfAnalysis.toString() + buildConfigField "boolean", "CI", ciBuild.toString() + buildConfigField "boolean", "RUNTIME_PERF_ANALYSIS", perfAnalysis.toString() javaCompileOptions { annotationProcessorOptions { @@ -121,7 +136,7 @@ android { testInstrumentationRunnerArgument "TEST_SERVER_URL", "${NC_TEST_SERVER_BASEURL}" testInstrumentationRunnerArgument "TEST_SERVER_USERNAME", "${NC_TEST_SERVER_USERNAME}" testInstrumentationRunnerArgument "TEST_SERVER_PASSWORD", "${NC_TEST_SERVER_PASSWORD}" - testInstrumentationRunnerArguments disableAnalytics: 'true' + testInstrumentationRunnerArguments disableAnalytics: "true" versionCode versionMajor * 10000000 + versionMinor * 10000 + versionPatch * 100 + versionBuild @@ -139,8 +154,15 @@ android { flavorDimensions += "default" buildTypes { + release { + buildConfigField "String", "NC_TEST_SERVER_DATA_STRING", "\"\"" + } + debug { - testCoverageEnabled(project.hasProperty('coverage')) + testCoverageEnabled = project.hasProperty("coverage") + resConfigs "xxxhdpi" + + buildConfigField "String", "NC_TEST_SERVER_DATA_STRING", "\"nc://login/user:${configProps['NC_TEST_SERVER_USERNAME']}&password:${configProps['NC_TEST_SERVER_PASSWORD']}&server:${configProps['NC_TEST_SERVER_BASEURL']}\"" } } @@ -151,17 +173,17 @@ android { productFlavors { // used for f-droid generic { - applicationId 'com.nextcloud.client' + applicationId "com.nextcloud.client" dimension "default" } gplay { - applicationId 'com.nextcloud.client' + applicationId "com.nextcloud.client" dimension "default" } huawei { - applicationId 'com.nextcloud.client' + applicationId "com.nextcloud.client" dimension "default" } @@ -180,10 +202,9 @@ android { } } - testOptions { unitTests.returnDefaultValues = true - animationsDisabled true + animationsDisabled = true } } @@ -191,17 +212,18 @@ android { // see http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Configuring-the-Structure packagingOptions { resources { - excludes += 'META-INF/LICENSE*' - pickFirst 'MANIFEST.MF' // workaround for duplicated manifest on some dependencies + excludes += "META-INF/LICENSE*" + excludes += "META-INF/versions/9/OSGI-INF/MANIFEST*" + pickFirst "MANIFEST.MF" // workaround for duplicated manifest on some dependencies } } tasks.register("checkstyle", Checkstyle) { configFile = file("${rootProject.projectDir}/checkstyle.xml") configProperties.checkstyleSuppressionsPath = file("${project.rootDir}/config/quality/checkstyle/suppressions.xml").absolutePath - source 'src' - include '**/*.java' - exclude '**/gen/**' + source "src" + include "**/*.java" + exclude "**/gen/**" classpath = files() } @@ -210,26 +232,26 @@ android { ignoreFailures = true // should continue checking ruleSets = [] - source 'src' - include '**/*.java' - exclude '**/gen/**' + source "src" + include "**/*.java" + exclude "**/gen/**" reports { xml { - destination = file("$project.buildDir/reports/pmd/pmd.xml") + destination = layout.buildDirectory.file("reports/pmd/pmd.xml").get().asFile } html { - destination = file("$project.buildDir/reports/pmd/pmd.html") + destination = layout.buildDirectory.file("reports/pmd/pmd.html").get().asFile } } } - check.dependsOn 'checkstyle', 'spotbugsGplayDebug', 'pmd', 'lint', 'spotlessKotlinCheck', 'detekt' + check.dependsOn "checkstyle", "spotbugsGplayDebug", "pmd", "lint", "spotlessKotlinCheck", "detekt" buildFeatures { - dataBinding true - viewBinding true - aidl true + dataBinding = true + viewBinding = true + aidl = true compose = true } @@ -243,11 +265,11 @@ android { } lint { - abortOnError false - checkGeneratedSources true - disable 'MissingTranslation', 'GradleDependency', 'VectorPath', 'IconMissingDensityFolder', 'IconDensities', 'GoogleAppIndexingWarning', 'MissingDefaultResource', 'InvalidPeriodicWorkRequestInterval', 'StringFormatInvalid', 'MissingQuantity' - htmlOutput file("$project.buildDir/reports/lint/lint.html") - htmlReport true + abortOnError = false + checkGeneratedSources = true + disable "MissingTranslation", "GradleDependency", "VectorPath", "IconMissingDensityFolder", "IconDensities", "GoogleAppIndexingWarning", "MissingDefaultResource", "InvalidPeriodicWorkRequestInterval", "StringFormatInvalid", "MissingQuantity" + htmlOutput = layout.buildDirectory.file("reports/lint/lint.html").get().asFile + htmlReport = true } sourceSets { @@ -255,189 +277,251 @@ android { androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } - composeOptions { - kotlinCompilerExtensionVersion = "1.5.11" + kapt { + useBuildCache = true } } dependencies { + // region Nextcloud library implementation("com.github.nextcloud:android-library:$androidLibraryVersion") { - exclude group: 'org.ogce', module: 'xpp3' // unused in Android and brings wrong Junit version + exclude group: "org.ogce", module: "xpp3" // unused in Android and brings wrong Junit version } + // endregion - // Jetpack Compose - implementation(platform("androidx.compose:compose-bom:2024.04.00")) - implementation("androidx.compose.ui:ui") - implementation("androidx.compose.ui:ui-graphics") - implementation("androidx.compose.material3:material3") - implementation("androidx.compose.ui:ui-tooling-preview:1.6.5") - debugImplementation 'androidx.compose.ui:ui-tooling:1.6.5' + // region Splash Screen + implementation libs.splashscreen + // endregion - compileOnly 'org.jbundle.util.osgi.wrapped:org.jbundle.util.osgi.wrapped.org.apache.http.client:4.1.2' - // remove after entire switch to lib v2 - implementation "commons-httpclient:commons-httpclient:3.1@jar" // remove after entire switch to lib v2 - implementation 'org.apache.jackrabbit:jackrabbit-webdav:2.13.5' // remove after entire switch to lib v2 - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'com.google.android.material:material:1.11.0' - implementation 'com.jakewharton:disklrucache:2.0.2' - implementation "androidx.appcompat:appcompat:$appCompatVersion" - implementation 'androidx.webkit:webkit:1.10.0' - implementation 'androidx.cardview:cardview:1.0.0' - implementation 'androidx.exifinterface:exifinterface:1.3.7' - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.1" - implementation "androidx.lifecycle:lifecycle-service:2.8.1" - implementation "androidx.work:work-runtime:$workRuntime" - implementation "androidx.work:work-runtime-ktx:$workRuntime" - implementation "androidx.fragment:fragment-ktx:1.6.2" - implementation 'com.github.albfernandez:juniversalchardet:2.0.3' // need this version for Android <7 - compileOnly 'com.google.code.findbugs:annotations:3.0.1u2' - implementation 'commons-io:commons-io:2.16.1' - implementation 'org.greenrobot:eventbus:3.3.1' - implementation 'com.googlecode.ez-vcard:ez-vcard:0.12.1' - implementation 'org.lukhnos:nnio:0.3' - implementation 'org.bouncycastle:bcpkix-jdk18on:1.77' - implementation 'com.google.code.gson:gson:2.10.1' - implementation 'com.github.nextcloud-deps:sectioned-recyclerview:0.6.1' - implementation 'com.github.chrisbanes:PhotoView:2.3.0' - implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.28' - implementation 'com.github.nextcloud-deps:qrcodescanner:0.1.2.4' // 'com.github.blikoon:QRCodeScanner:0.1.2' - implementation 'com.google.android.flexbox:flexbox:3.0.0' - implementation('com.github.bumptech.glide:glide:3.8.0') { - exclude group: "com.android.support" - } - implementation 'com.caverock:androidsvg:1.4' - implementation 'androidx.annotation:annotation:1.7.1' - implementation 'com.vanniktech:emoji-google:0.18.0' + // region Jetpack Compose + implementation(platform(libs.compose.bom)) + implementation(libs.compose.ui) + implementation(libs.compose.ui.graphics) + implementation(libs.compose.material3) + debugImplementation(libs.compose.ui.tooling) + implementation(libs.compose.ui.tooling.preview) + // endregion - implementation "com.github.nextcloud-deps.hwsecurity:hwsecurity-fido:$fidoVersion" - implementation "com.github.nextcloud-deps.hwsecurity:hwsecurity-fido2:$fidoVersion" + // region Media3 + implementation libs.media3.ui + implementation libs.media3.session + implementation libs.media3.exoplayer + implementation libs.media3.datasource + // endregion - // document scanner not available on FDroid (generic) due to OpenCV binaries - gplayImplementation project(':appscan') - huaweiImplementation project(':appscan') - qaImplementation project(':appscan') + // region Room + implementation libs.room.runtime + ksp "androidx.room:room-compiler:$roomVersion" + androidTestImplementation libs.room.testing + // endregion - spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.13.0' - spotbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.6.4' + // region Espresso + androidTestImplementation libs.espresso.core + androidTestImplementation libs.espresso.contrib + androidTestImplementation libs.espresso.web + androidTestImplementation libs.espresso.accessibility + androidTestImplementation libs.espresso.intents + androidTestImplementation libs.espresso.idling.resource + // endregion - implementation "com.google.dagger:dagger:$daggerVersion" - implementation "com.google.dagger:dagger-android:$daggerVersion" - implementation "com.google.dagger:dagger-android-support:$daggerVersion" + // region Glide + implementation libs.glide + ksp libs.ksp + // endregion + + // region UI + implementation libs.appcompat + implementation libs.webkit + implementation libs.cardview + implementation libs.exifinterface + implementation libs.fragment.ktx + // endregion + + // region Worker + implementation libs.work.runtime + implementation libs.work.runtime.ktx + // endregion + + // region Lifecycle + implementation libs.lifecycle.viewmodel.ktx + implementation libs.lifecycle.service + implementation(libs.lifecycle.runtime.ktx) + // endregion + + // region JUnit + androidTestImplementation libs.junit + androidTestImplementation libs.rules + androidTestImplementation libs.runner + androidTestUtil libs.orchestrator + androidTestImplementation libs.core.ktx + androidTestImplementation libs.core.testing + // endregion + + // region other libraries + compileOnly libs.org.jbundle.util.osgi.wrapped.org.apache.http.client + implementation libs.commons.httpclient.commons.httpclient // remove after entire switch to lib v2 + implementation libs.jackrabbit.webdav // remove after entire switch to lib v2 + implementation libs.constraintlayout + implementation libs.legacy.support.v4 + implementation libs.material + implementation libs.disklrucache + implementation libs.juniversalchardet // need this version for Android <7 + compileOnly libs.annotations + implementation libs.commons.io + implementation libs.eventbus + implementation libs.ez.vcard + implementation libs.nnio + implementation libs.bcpkix.jdk18on + implementation libs.gson + implementation libs.sectioned.recyclerview + implementation libs.photoview + implementation libs.android.gif.drawable + implementation libs.qrcodescanner // "com.github.blikoon:QRCodeScanner:0.1.2" + implementation libs.flexbox + implementation libs.androidsvg + implementation libs.annotation + implementation libs.emoji.google + // endregion + + // region AppScan, document scanner not available on FDroid (generic) due to OpenCV binaries + gplayImplementation project(":appscan") + huaweiImplementation project(":appscan") + qaImplementation project(":appscan") + // endregion + + // region SpotBugs + spotbugsPlugins libs.findsecbugs.plugin + spotbugsPlugins libs.fb.contrib + // endregion + + // region Dagger + implementation libs.dagger + implementation libs.dagger.android + implementation libs.dagger.android.support kapt "com.google.dagger:dagger-compiler:$daggerVersion" kapt "com.google.dagger:dagger-android-processor:$daggerVersion" + // endregion - implementation 'org.conscrypt:conscrypt-android:2.5.2' + // region Crypto + implementation libs.conscrypt.android + // endregion - implementation "androidx.media3:media3-ui:$androidxMediaVersion" - implementation "androidx.media3:media3-exoplayer:$androidxMediaVersion" - implementation "androidx.media3:media3-datasource-okhttp:$androidxMediaVersion" + // region Library + implementation libs.library + // endregion - implementation 'me.zhanghai.android.fastscroll:library:1.3.0' + // region Shimmer + implementation libs.loaderviewlibrary + // endregion - // Shimmer animation - implementation 'io.github.elye:loaderviewlibrary:3.0.0' - - // dependencies for markdown rendering - implementation "io.noties.markwon:core:$markwonVersion" - implementation "io.noties.markwon:ext-strikethrough:$markwonVersion" - implementation "io.noties.markwon:ext-tables:$markwonVersion" - implementation "io.noties.markwon:ext-tasklist:$markwonVersion" - implementation "io.noties.markwon:html:$markwonVersion" - - implementation "io.noties.markwon:syntax-highlight:$markwonVersion" - implementation "io.noties:prism4j:$prismVersion" + // region Markdown rendering + implementation libs.core + implementation libs.ext.strikethrough + implementation libs.ext.tables + implementation libs.ext.tasklist + implementation libs.html + implementation libs.syntax.highlight + implementation libs.prism4j kapt "io.noties:prism4j-bundler:$prismVersion" + // endregion - // dependencies for image cropping and rotation - implementation 'com.vanniktech:android-image-cropper:4.5.0' + // region Image cropping / rotation + implementation libs.android.image.cropper + // endregion - implementation 'org.osmdroid:osmdroid-android:6.1.18' + // region Maps + implementation libs.osmdroid.android + // endregion - implementation('org.mnode.ical4j:ical4j:3.0.0') { - ['org.apache.commons', 'commons-logging'].each { + // region iCal4j + implementation(libs.ical4j) { + ["org.apache.commons", "commons-logging"].each { exclude group: "$it" } } + // endregion + // region LeakCanary if (perfAnalysis) { - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.13' + debugImplementation "com.squareup.leakcanary:leakcanary-android:2.14" } + // endregion - // dependencies for local unit tests - testImplementation 'junit:junit:4.13.2' - testImplementation "org.mockito:mockito-core:$mockitoVersion" - testImplementation "androidx.test:core:$androidxTestVersion" - testImplementation 'org.json:json:20240303' - testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" - testImplementation 'androidx.arch.core:core-testing:2.2.0' + // region Local Unit Test + testImplementation libs.junit.junit + testImplementation libs.mockito.core + testImplementation libs.test.core + testImplementation libs.json + testImplementation libs.mockito.kotlin + testImplementation libs.core.testing testImplementation "io.mockk:mockk:$mockkVersion" - testImplementation "io.mockk:mockk-android:$mockkVersion" + testImplementation libs.mockk.android + // endregion - // dependencies for instrumented tests - // JUnit4 Rules - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation "androidx.test:rules:$androidxTestVersion" - // Android JUnit Runner - androidTestImplementation "androidx.test:runner:1.5.2" - androidTestUtil "androidx.test:orchestrator:1.4.2" - androidTestImplementation "androidx.test:core-ktx:$androidxTestVersion" - - // Espresso - androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" - androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion" - androidTestImplementation "androidx.test.espresso:espresso-web:$espressoVersion" - androidTestImplementation "androidx.test.espresso:espresso-accessibility:$espressoVersion" - androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion" - androidTestImplementation "androidx.test.espresso:espresso-idling-resource:$espressoVersion" - - // Mocking support - androidTestImplementation 'com.github.tmurakami:dexopener:2.0.5' // required to allow mocking on API 27 and older - androidTestImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" - androidTestImplementation "org.mockito:mockito-core:$mockitoVersion" - androidTestImplementation("org.mockito:mockito-android:$mockitoVersion") - androidTestImplementation "io.mockk:mockk-android:$mockkVersion" - androidTestImplementation 'androidx.arch.core:core-testing:2.2.0' - androidTestImplementation "com.facebook.testing.screenshot:core:0.15.0" + // region Mocking support + androidTestImplementation libs.dexopener // required to allow mocking on API 27 and older + androidTestImplementation libs.mockito.kotlin + androidTestImplementation libs.mockito.core + androidTestImplementation(libs.mockito.android) + androidTestImplementation libs.mockk.android + androidTestImplementation libs.screenshot.core + // endregion + // region UIAutomator // UIAutomator - for cross-app UI tests, and to grant screen is turned on in Espresso tests - // androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' + // androidTestImplementation "androidx.test.uiautomator:uiautomator:2.2.0" // fix conflict in dependencies; see http://g.co/androidstudio/app-test-app-conflict for details - //androidTestImplementation "com.android.support:support-annotations:${supportLibraryVersion}" - androidTestImplementation 'tools.fastlane:screengrab:2.1.1' - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + // androidTestImplementation "com.android.support:support-annotations:${supportLibraryVersion}" + androidTestImplementation libs.screengrab + // endregion - implementation "com.github.stateless4j:stateless4j:2.6.0" + // region Kotlin + implementation libs.kotlin.stdlib + // endregion - // upon each update first test: new registration, receive push - gplayImplementation "com.google.firebase:firebase-messaging:23.4.1" - gplayImplementation 'com.google.android.play:review-ktx:2.0.1' + // region Stateless + implementation libs.stateless4j + // endregion - implementation 'com.github.nextcloud.android-common:ui:0.17.0' + // region Google Play dependencies, upon each update first test: new registration, receive push + gplayImplementation libs.firebase.messaging + gplayImplementation libs.play.services.base + gplayImplementation libs.review.ktx + // endregion - implementation "androidx.room:room-runtime:$roomVersion" - ksp "androidx.room:room-compiler:$roomVersion" - androidTestImplementation "androidx.room:room-testing:$roomVersion" + // region UI + implementation libs.ui + // endregion - implementation "io.coil-kt:coil:2.6.0" - - // splash screen dependency ref: https://developer.android.com/develop/ui/views/launch/splash-screen/migrate - implementation 'androidx.core:core-splashscreen:1.0.1' + // region Image loading + implementation libs.coil + // endregion } + configurations.configureEach { resolutionStrategy { - cacheChangingModulesFor 0, 'seconds' - force 'org.objenesis:objenesis:3.3' + force "org.objenesis:objenesis:3.4" eachDependency { details -> - if ('org.jacoco' == details.requested.group) { + if ("org.jacoco" == details.requested.group) { details.useVersion "$jacoco_version" } } } } +// Run the compiler as a separate process +tasks.withType(JavaCompile).configureEach { + options.fork = true + + // Enable Incremental Compilation + options.incremental = true +} + tasks.withType(Test).configureEach { + // Run tests in parallel + maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 + // increased logging for tests testLogging { events "passed", "skipped", "failed" @@ -465,7 +549,7 @@ if (shotTest) { showOnlyFailingTestsInReports = ciBuild // CI environment renders some shadows slightly different from local VMs // Add a 0.5% tolerance to account for that - tolerance = ciBuild ? 0.5 : 0 + tolerance = ciBuild ? 0.1 : 0 } } @@ -476,7 +560,7 @@ jacoco { spotbugs { ignoreFailures = true // should continue checking effort = Effort.MAX - reportLevel = Confidence.valueOf('MEDIUM') + reportLevel = Confidence.valueOf("MEDIUM") } tasks.withType(SpotBugsTask){task -> @@ -484,7 +568,7 @@ tasks.withType(SpotBugsTask){task -> String variantName = variantNameCap.substring(0, 1).toLowerCase() + variantNameCap.substring(1) dependsOn "compile${variantNameCap}Sources" - classes = fileTree("$project.buildDir/intermediates/javac/${variantName}/compile${variantNameCap}JavaWithJavac/classes/") + classes = fileTree(layout.buildDirectory.get().asFile.toString()+"/intermediates/javac/${variantName}/compile${variantNameCap}JavaWithJavac/classes/") excludeFilter = file("${project.rootDir}/scripts/analysis/spotbugs-filter.xml") reports { xml { @@ -492,12 +576,12 @@ tasks.withType(SpotBugsTask){task -> } html { required = true - outputLocation = file("$project.buildDir/reports/spotbugs/spotbugs.html") - stylesheet = 'fancy.xsl' + outputLocation = layout.buildDirectory.file("reports/spotbugs/spotbugs.html").get().asFile + stylesheet = "fancy.xsl" } } } ksp { - arg('room.schemaLocation', "$projectDir/schemas") + arg("room.schemaLocation", "$projectDir/schemas") } diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..7c5764b --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,522 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Jimly Asshiddiqy + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +@file:Suppress("UnstableApiUsage", "DEPRECATION") + +import com.android.build.gradle.internal.api.ApkVariantOutputImpl +import com.github.spotbugs.snom.Confidence +import com.github.spotbugs.snom.Effort +import com.github.spotbugs.snom.SpotBugsTask +import com.karumi.shot.ShotExtension +import org.gradle.internal.jvm.Jvm +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import java.io.FileInputStream +import java.util.Properties + +val shotTest = System.getenv("SHOT_TEST") == "true" +val ciBuild = System.getenv("CI") == "true" +val perfAnalysis = project.hasProperty("perfAnalysis") + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.spotless) + alias(libs.plugins.kapt) + alias(libs.plugins.ksp) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.jetbrains.kotlin.android) + alias(libs.plugins.spotbugs) + alias(libs.plugins.detekt) + // needed to make renovate run without shot, as shot requires Android SDK + // https://github.com/pedrovgs/Shot/issues/300 + if (System.getenv("SHOT_TEST") == "true") alias(libs.plugins.shot) + id("checkstyle") + id("pmd") +} +apply(from = "${rootProject.projectDir}/jacoco.gradle.kts") + +println("Gradle uses Java ${Jvm.current()}") + +configurations.configureEach { + // via prism4j, already using annotations explicitly + exclude(group = "org.jetbrains", module = "annotations-java5") + + resolutionStrategy { + force(libs.objenesis) + + eachDependency { + if (requested.group == "org.checkerframework" && requested.name != "checker-compat-qual") { + useVersion(libs.versions.checker.get()) + because("https://github.com/google/ExoPlayer/issues/10007") + } else if (requested.group == "org.jacoco") { + useVersion(libs.versions.jacoco.get()) + } else if (requested.group == "commons-logging" && requested.name == "commons-logging") { + useTarget(libs.slfj) + } + } + } +} + +// semantic versioning for version code +val versionMajor = 3 +val versionMinor = 35 +val versionPatch = 0 +val versionBuild = 0 // 0-50=Alpha / 51-98=RC / 90-99=stable + +val ndkEnv = buildMap { + file("${project.rootDir}/ndk.env").readLines().forEach { + val (key, value) = it.split("=") + put(key, value) + } +} + +val configProps = Properties().apply { + val file = rootProject.file(".gradle/config.properties") + if (file.exists()) load(FileInputStream(file)) +} + +val ncTestServerUsername = configProps["NC_TEST_SERVER_USERNAME"] +val ncTestServerPassword = configProps["NC_TEST_SERVER_PASSWORD"] +val ncTestServerBaseUrl = configProps["NC_TEST_SERVER_BASEURL"] + +android { + // install this NDK version and Cmake to produce smaller APKs. Build will still work if not installed + ndkVersion = "${ndkEnv["NDK_VERSION"]}" + + namespace = "com.owncloud.android" + testNamespace = "${namespace}.test" + + androidResources.generateLocaleConfig = true + + defaultConfig { + applicationId = "com.nextcloud.client" + minSdk = 27 + targetSdk = 36 + compileSdk = 36 + + buildConfigField("boolean", "CI", ciBuild.toString()) + buildConfigField("boolean", "RUNTIME_PERF_ANALYSIS", perfAnalysis.toString()) + + javaCompileOptions.annotationProcessorOptions { + arguments += mapOf("room.schemaLocation" to "$projectDir/schemas") + } + + // arguments to be passed to functional tests + testInstrumentationRunner = if (shotTest) "com.karumi.shot.ShotTestRunner" + else "com.nextcloud.client.TestRunner" + + testInstrumentationRunnerArguments += mapOf( + "TEST_SERVER_URL" to ncTestServerBaseUrl.toString(), + "TEST_SERVER_USERNAME" to ncTestServerUsername.toString(), + "TEST_SERVER_PASSWORD" to ncTestServerPassword.toString() + ) + testInstrumentationRunnerArguments["disableAnalytics"] = "true" + + versionCode = versionMajor * 10000000 + versionMinor * 10000 + versionPatch * 100 + versionBuild + versionName = when { + versionBuild > 89 -> "${versionMajor}.${versionMinor}.${versionPatch}" + versionBuild > 50 -> "${versionMajor}.${versionMinor}.${versionPatch} RC" + (versionBuild - 50) + else -> "${versionMajor}.${versionMinor}.${versionPatch} Alpha" + (versionBuild + 1) + } + + // adapt structure from Eclipse to Gradle/Android Studio expectations; + // see http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Configuring-the-Structure + + flavorDimensions += "default" + + buildTypes { + release { + buildConfigField("String", "NC_TEST_SERVER_DATA_STRING", "\"\"") + } + + debug { + enableUnitTestCoverage = project.hasProperty("coverage") + resConfigs("xxxhdpi") + + buildConfigField( + "String", + "NC_TEST_SERVER_DATA_STRING", + "\"nc://login/user:${ncTestServerUsername}&password:${ncTestServerPassword}&server:${ncTestServerBaseUrl}\"" + ) + } + } + + productFlavors { + // used for f-droid + register("generic") { + applicationId = "com.nextcloud.client" + dimension = "default" + } + + register("gplay") { + applicationId = "com.nextcloud.client" + dimension = "default" + } + + register("huawei") { + applicationId = "com.nextcloud.client" + dimension = "default" + } + + register("versionDev") { + applicationId = "com.nextcloud.android.beta" + dimension = "default" + versionCode = 20220322 + versionName = "20220322" + } + + register("qa") { + applicationId = "com.nextcloud.android.qa" + dimension = "default" + versionCode = 1 + versionName = "1" + } + } + } + + applicationVariants.configureEach { + outputs.configureEach { + if (this is ApkVariantOutputImpl) this.outputFileName = "${this.baseName}-${this.versionCode}.apk" + } + } + + testOptions { + unitTests.isReturnDefaultValues = true + animationsDisabled = true + } + + // adapt structure from Eclipse to Gradle/Android Studio expectations; + // see http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Configuring-the-Structure + packaging.resources { + excludes.addAll(listOf("META-INF/LICENSE*", "META-INF/versions/9/OSGI-INF/MANIFEST*")) + pickFirsts.add("MANIFEST.MF") // workaround for duplicated manifest on some dependencies + } + + buildFeatures { + buildConfig = true + dataBinding = true + viewBinding = true + aidl = true + compose = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + lint { + abortOnError = true + warningsAsErrors = true + checkGeneratedSources = true + disable.addAll( + listOf( + "MissingTranslation", + "GradleDependency", + "VectorPath", + "IconMissingDensityFolder", + "IconDensities", + "GoogleAppIndexingWarning", + "MissingDefaultResource", + "InvalidPeriodicWorkRequestInterval", + "StringFormatInvalid", + "MissingQuantity", + "IconXmlAndPng", + "SelectedPhotoAccess", + "UnsafeIntentLaunch" + ) + ) + htmlOutput = layout.buildDirectory.file("reports/lint/lint.html").get().asFile + htmlReport = true + } + + sourceSets { + // Adds exported schema location as test app assets. + getByName("androidTest") { + assets.srcDirs(files("$projectDir/schemas")) + } + } + +} + +kapt.useBuildCache = true + +ksp.arg("room.schemaLocation", "$projectDir/schemas") + +kotlin.compilerOptions.jvmTarget.set(JvmTarget.JVM_17) + +spotless.kotlin { + target("**/*.kt") + ktlint() +} + +detekt.config.setFrom("detekt.yml") + +if (shotTest) configure { + showOnlyFailingTestsInReports = ciBuild + // CI environment renders some shadows slightly different from local VMs + // Add a 0.5% tolerance to account for that + tolerance = if (ciBuild) 0.1 else 0.0 +} + + +spotbugs { + ignoreFailures = true // should continue checking + effort = Effort.MAX + reportLevel = Confidence.valueOf("MEDIUM") +} + +tasks.register("checkstyle") { + configFile = file("${rootProject.projectDir}/checkstyle.xml") + setConfigProperties( + "checkstyleSuppressionsPath" to file("${rootProject.rootDir}/suppressions.xml").absolutePath + ) + source("src") + include("**/*.java") + exclude("**/gen/**") + classpath = files() +} + +tasks.register("pmd") { + ruleSetFiles = files("${rootProject.rootDir}/ruleset.xml") + ignoreFailures = true // should continue checking + ruleSets = emptyList() + + source("src") + include("**/*.java") + exclude("**/gen/**") + + reports { + xml.outputLocation.set(layout.buildDirectory.file("reports/pmd/pmd.xml").get().asFile) + html.outputLocation.set(layout.buildDirectory.file("reports/pmd/pmd.html").get().asFile) + } +} + +tasks.withType().configureEach { + val variantNameCap = name.replace("spotbugs", "") + val variantName = variantNameCap.substring(0, 1).lowercase() + variantNameCap.substring(1) + dependsOn("compile${variantNameCap}Sources") + + classes = fileTree( + layout.buildDirectory.get().asFile.toString() + + "/intermediates/javac/${variantName}/compile${variantNameCap}JavaWithJavac/classes/" + ) + excludeFilter.set(file("${project.rootDir}/scripts/analysis/spotbugs-filter.xml")) + + reports.create("xml") { + required.set(true) + } + reports.create("html") { + required.set(true) + outputLocation.set(layout.buildDirectory.file("reports/spotbugs/spotbugs.html")) + setStylesheet("fancy.xsl") + } +} + +// Run the compiler as a separate process +tasks.withType().configureEach { + options.isFork = true + + // Enable Incremental Compilation + options.isIncremental = true +} + +tasks.withType().configureEach { + // Run tests in parallel + maxParallelForks = Runtime.getRuntime().availableProcessors().div(2) + + // increased logging for tests + testLogging.events("passed", "skipped", "failed") +} + +tasks.named("check").configure { + dependsOn("checkstyle", "spotbugsGplayDebug", "pmd", "lint", "spotlessKotlinCheck", "detekt") +} + +dependencies { + // region Nextcloud library + implementation(libs.android.library) { + exclude(group = "org.ogce", module = "xpp3") // unused in Android and brings wrong Junit version + } + // endregion + + // region Splash Screen + implementation(libs.splashscreen) + // endregion + + // region Jetpack Compose + implementation(platform(libs.compose.bom)) + implementation(libs.material.icons.core) + implementation(libs.compose.ui) + implementation(libs.compose.ui.graphics) + implementation(libs.compose.material3) + implementation(libs.compose.ui.tooling.preview) + debugImplementation(libs.compose.ui.tooling) + // endregion + + // region Media3 + implementation(libs.bundles.media3) + // endregion + + // region Room + implementation(libs.room.runtime) + ksp(libs.room.compiler) + androidTestImplementation(libs.room.testing) + // endregion + + // region Espresso + androidTestImplementation(libs.bundles.espresso) + // endregion + + // region Glide + implementation(libs.glide) + ksp(libs.ksp) + // endregion + + // region UI + implementation(libs.bundles.ui) + // endregion + + // region Worker + implementation(libs.work.runtime) + implementation(libs.work.runtime.ktx) + // endregion + + // region Lifecycle + implementation(libs.lifecycle.viewmodel.ktx) + implementation(libs.lifecycle.service) + implementation(libs.lifecycle.runtime.ktx) + // endregion + + // region JUnit + androidTestImplementation(libs.junit) + androidTestImplementation(libs.rules) + androidTestImplementation(libs.runner) + androidTestUtil(libs.orchestrator) + androidTestImplementation(libs.core.ktx) + androidTestImplementation(libs.core.testing) + // endregion + + // region other libraries + compileOnly(libs.org.jbundle.util.osgi.wrapped.org.apache.http.client) + implementation(libs.commons.httpclient.commons.httpclient) // remove after entire switch to lib v2 + implementation(libs.jackrabbit.webdav) // remove after entire switch to lib v2 + implementation(libs.constraintlayout) + implementation(libs.legacy.support.v4) + implementation(libs.material) + implementation(libs.disklrucache) + implementation(libs.juniversalchardet) // need this version for Android <7 + compileOnly(libs.annotations) + implementation(libs.commons.io) + implementation(libs.eventbus) + implementation(libs.ez.vcard) + implementation(libs.nnio) + implementation(libs.bcpkix.jdk18on) + implementation(libs.gson) + implementation(libs.sectioned.recyclerview) + implementation(libs.photoview) + implementation(libs.android.gif.drawable) + implementation(libs.qrcodescanner) // "com.github.blikoon:QRCodeScanner:0.1.2" + implementation(libs.flexbox) + implementation(libs.androidsvg) + implementation(libs.annotation) + implementation(libs.emoji.google) + // endregion + + // region AppScan, document scanner not available on FDroid (generic) due to OpenCV binaries + "gplayImplementation"(project(":appscan")) + "huaweiImplementation"(project(":appscan")) + "qaImplementation"(project(":appscan")) + // endregion + + // region SpotBugs + spotbugsPlugins(libs.findsecbugs.plugin) + spotbugsPlugins(libs.fb.contrib) + // endregion + + // region Dagger + implementation(libs.dagger) + implementation(libs.dagger.android) + implementation(libs.dagger.android.support) + ksp(libs.dagger.compiler) + ksp(libs.dagger.processor) + // endregion + + // region Crypto + implementation(libs.conscrypt.android) + // endregion + + // region Library + implementation(libs.library) + // endregion + + // region Shimmer + implementation(libs.loaderviewlibrary) + // endregion + + // region Markdown rendering + implementation(libs.bundles.markdown.rendering) + kapt(libs.prism4j.bundler) + // endregion + + // region Image cropping / rotation + implementation(libs.android.image.cropper) + // endregion + + // region Maps + implementation(libs.osmdroid.android) + // endregion + + // region iCal4j + implementation(libs.ical4j) { + listOf("org.apache.commons", "commons-logging").forEach { groupName -> exclude(group = groupName) } + } + // endregion + + // region LeakCanary + if (perfAnalysis) debugImplementation(libs.leakcanary) + // endregion + + // region Local Unit Test + testImplementation(libs.bundles.unit.test) + // endregion + + // region Mocking support + androidTestImplementation(libs.bundles.mocking) + // endregion + + // region UIAutomator + // UIAutomator - for cross-app UI tests, and to grant screen is turned on in Espresso tests + // androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0" + // fix conflict in dependencies; see http://g.co/androidstudio/app-test-app-conflict for details + // androidTestImplementation("com.android.support:support-annotations:${supportLibraryVersion}" + androidTestImplementation(libs.screengrab) + // endregion + + // region Kotlin + implementation(libs.kotlin.stdlib) + // endregion + + // region Stateless + implementation(libs.stateless4j) + // endregion + + // region Google Play dependencies, upon each update first test: new registration, receive push + "gplayImplementation"(libs.bundles.gplay) + // endregion + + // region UI + implementation(libs.ui) + // endregion + + // region Image loading + implementation(libs.coil) + // endregion + + // kotlinx.serialization + implementation(libs.kotlinx.serialization.json) +} diff --git a/app/detekt.yml b/app/detekt.yml index 3e8ba26..91abf2b 100644 --- a/app/detekt.yml +++ b/app/detekt.yml @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors -# SPDX-License-Identifier: AGPL-3.0-or-later +# SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only build: maxIssues: 2 weights: diff --git a/app/lint.xml b/app/lint.xml index a157f37..2316b42 100644 --- a/app/lint.xml +++ b/app/lint.xml @@ -1,12 +1,12 @@ @@ -22,19 +22,27 @@ - + regexp="screenshot_01_gridView|screenshot_02_listView|screenshot_03_drawer|screenshot_04_accounts|screenshot_05_autoUpload|screenshot_06_davdroid" /> + + + + + + - - + + + + + @@ -56,7 +64,7 @@ - + @@ -77,4 +85,8 @@ + + + + diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/81.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/81.json index 10d076c..3dd2580 100644 --- a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/81.json +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/81.json @@ -1206,4 +1206,4 @@ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '082a63031678a67879428f688f02d3b5')" ] } -} \ No newline at end of file +} diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/82.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/82.json new file mode 100644 index 0000000..e16d7c1 --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/82.json @@ -0,0 +1,1233 @@ +{ + "formatVersion": 1, + "database": { + "version": 82, + "identityHash": "e78b1402db9da7caff78c46fff585672", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `forbidden_filename_characters` INTEGER, `forbidden_filenames` INTEGER, `forbidden_filename_extensions` INTEGER, `forbidden_filename_basenames` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e78b1402db9da7caff78c46fff585672')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/83.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/83.json new file mode 100644 index 0000000..c27bba8 --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/83.json @@ -0,0 +1,1245 @@ +{ + "formatVersion": 1, + "database": { + "version": 83, + "identityHash": "365a8731a100a61ae5029beb74acd02e", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `forbidden_filename_characters` INTEGER, `forbidden_filenames` INTEGER, `forbidden_filename_extensions` INTEGER, `forbidden_filename_basenames` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '365a8731a100a61ae5029beb74acd02e')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/84.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/84.json new file mode 100644 index 0000000..b703cbe --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/84.json @@ -0,0 +1,1301 @@ +{ + "formatVersion": 1, + "database": { + "version": 84, + "identityHash": "70f2e2adb603afda7f87dbfb3b902e02", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `forbidden_filename_characters` INTEGER, `forbidden_filenames` INTEGER, `forbidden_filename_extensions` INTEGER, `forbidden_filename_basenames` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "offline_operations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `offline_operations_parent_oc_file_id` INTEGER, `offline_operations_parent_path` TEXT, `offline_operations_type` TEXT, `offline_operations_path` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "parentOCFileId", + "columnName": "offline_operations_parent_oc_file_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "parentPath", + "columnName": "offline_operations_parent_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "offline_operations_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "offline_operations_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filename", + "columnName": "offline_operations_file_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "offline_operations_created_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '70f2e2adb603afda7f87dbfb3b902e02')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/85.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/85.json new file mode 100644 index 0000000..5e2a33b --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/85.json @@ -0,0 +1,1301 @@ +{ + "formatVersion": 1, + "database": { + "version": 85, + "identityHash": "2d24b9210a36150f221156d2e8f59665", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `forbidden_filename_characters` INTEGER, `forbidden_filenames` INTEGER, `forbidden_filename_extensions` INTEGER, `forbidden_filename_basenames` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "offline_operations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `offline_operations_parent_oc_file_id` INTEGER, `offline_operations_path` TEXT, `offline_operations_type` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER, `offline_operations_modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "parentOCFileId", + "columnName": "offline_operations_parent_oc_file_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "offline_operations_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "offline_operations_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filename", + "columnName": "offline_operations_file_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "offline_operations_created_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modifiedAt", + "columnName": "offline_operations_modified_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2d24b9210a36150f221156d2e8f59665')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/86.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/86.json new file mode 100644 index 0000000..2571f61 --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/86.json @@ -0,0 +1,1331 @@ +{ + "formatVersion": 1, + "database": { + "version": 86, + "identityHash": "277489b9d4a6ee84f96d09dea39591ba", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `forbidden_filename_characters` INTEGER, `forbidden_filenames` INTEGER, `forbidden_filename_extensions` INTEGER, `forbidden_filename_basenames` INTEGER, `files_download_limit` INTEGER, `files_download_limit_default` INTEGER, `recommendation` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesDownloadLimit", + "columnName": "files_download_limit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesDownloadLimitDefault", + "columnName": "files_download_limit_default", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT, `download_limit_limit` INTEGER, `download_limit_count` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadLimitLimit", + "columnName": "download_limit_limit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "downloadLimitCount", + "columnName": "download_limit_count", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "offline_operations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `offline_operations_parent_oc_file_id` INTEGER, `offline_operations_path` TEXT, `offline_operations_type` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER, `offline_operations_modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "parentOCFileId", + "columnName": "offline_operations_parent_oc_file_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "offline_operations_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "offline_operations_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filename", + "columnName": "offline_operations_file_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "offline_operations_created_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modifiedAt", + "columnName": "offline_operations_modified_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '277489b9d4a6ee84f96d09dea39591ba')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/87.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/87.json new file mode 100644 index 0000000..2bce6bc --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/87.json @@ -0,0 +1,1337 @@ +{ + "formatVersion": 1, + "database": { + "version": 87, + "identityHash": "c67369ca15672b4c84289aa188f49e50", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `forbidden_filename_characters` INTEGER, `forbidden_filenames` INTEGER, `forbidden_filename_extensions` INTEGER, `forbidden_filename_basenames` INTEGER, `files_download_limit` INTEGER, `files_download_limit_default` INTEGER, `recommendation` INTEGER, `notes_folder_path` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesDownloadLimit", + "columnName": "files_download_limit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesDownloadLimitDefault", + "columnName": "files_download_limit_default", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notesFolderPath", + "columnName": "notes_folder_path", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT, `download_limit_limit` INTEGER, `download_limit_count` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadLimitLimit", + "columnName": "download_limit_limit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "downloadLimitCount", + "columnName": "download_limit_count", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "offline_operations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `offline_operations_parent_oc_file_id` INTEGER, `offline_operations_path` TEXT, `offline_operations_type` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER, `offline_operations_modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "parentOCFileId", + "columnName": "offline_operations_parent_oc_file_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "offline_operations_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "offline_operations_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filename", + "columnName": "offline_operations_file_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "offline_operations_created_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modifiedAt", + "columnName": "offline_operations_modified_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c67369ca15672b4c84289aa188f49e50')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/88.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/88.json new file mode 100644 index 0000000..9255924 --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/88.json @@ -0,0 +1,1343 @@ +{ + "formatVersion": 1, + "database": { + "version": 88, + "identityHash": "72369823c54307097d8ca60cf6944e2a", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `forbidden_filename_characters` INTEGER, `forbidden_filenames` INTEGER, `forbidden_filename_extensions` INTEGER, `forbidden_filename_basenames` INTEGER, `files_download_limit` INTEGER, `files_download_limit_default` INTEGER, `recommendation` INTEGER, `notes_folder_path` TEXT, `default_permissions` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesDownloadLimit", + "columnName": "files_download_limit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesDownloadLimitDefault", + "columnName": "files_download_limit_default", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notesFolderPath", + "columnName": "notes_folder_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "defaultPermissions", + "columnName": "default_permissions", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT, `download_limit_limit` INTEGER, `download_limit_count` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadLimitLimit", + "columnName": "download_limit_limit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "downloadLimitCount", + "columnName": "download_limit_count", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "offline_operations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `offline_operations_parent_oc_file_id` INTEGER, `offline_operations_path` TEXT, `offline_operations_type` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER, `offline_operations_modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "parentOCFileId", + "columnName": "offline_operations_parent_oc_file_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "offline_operations_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "offline_operations_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filename", + "columnName": "offline_operations_file_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "offline_operations_created_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modifiedAt", + "columnName": "offline_operations_modified_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '72369823c54307097d8ca60cf6944e2a')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/89.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/89.json new file mode 100644 index 0000000..e54001f --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/89.json @@ -0,0 +1,1349 @@ +{ + "formatVersion": 1, + "database": { + "version": 89, + "identityHash": "7a70f9151914c24eb0e5350316b126f5", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `forbidden_filename_characters` INTEGER, `forbidden_filenames` INTEGER, `forbidden_filename_extensions` INTEGER, `forbidden_filename_basenames` INTEGER, `files_download_limit` INTEGER, `files_download_limit_default` INTEGER, `recommendation` INTEGER, `notes_folder_path` TEXT, `default_permissions` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesDownloadLimit", + "columnName": "files_download_limit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesDownloadLimitDefault", + "columnName": "files_download_limit_default", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notesFolderPath", + "columnName": "notes_folder_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "defaultPermissions", + "columnName": "default_permissions", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT, `uploaded` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT, `download_limit_limit` INTEGER, `download_limit_count` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadLimitLimit", + "columnName": "download_limit_limit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "downloadLimitCount", + "columnName": "download_limit_count", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "offline_operations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `offline_operations_parent_oc_file_id` INTEGER, `offline_operations_path` TEXT, `offline_operations_type` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER, `offline_operations_modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "parentOCFileId", + "columnName": "offline_operations_parent_oc_file_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "offline_operations_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "offline_operations_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filename", + "columnName": "offline_operations_file_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "offline_operations_created_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modifiedAt", + "columnName": "offline_operations_modified_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7a70f9151914c24eb0e5350316b126f5')" + ] + } +} diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/90.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/90.json new file mode 100644 index 0000000..c0b53e5 --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/90.json @@ -0,0 +1,1355 @@ +{ + "formatVersion": 1, + "database": { + "version": 90, + "identityHash": "93eb4d5fbf952984b6fc2df9f7c369e1", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `forbidden_filename_characters` INTEGER, `forbidden_filenames` INTEGER, `forbidden_filename_extensions` INTEGER, `forbidden_filename_basenames` INTEGER, `files_download_limit` INTEGER, `files_download_limit_default` INTEGER, `recommendation` INTEGER, `notes_folder_path` TEXT, `default_permissions` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesDownloadLimit", + "columnName": "files_download_limit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filesDownloadLimitDefault", + "columnName": "files_download_limit_default", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notesFolderPath", + "columnName": "notes_folder_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "defaultPermissions", + "columnName": "default_permissions", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT, `uploaded` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT, `download_limit_limit` INTEGER, `download_limit_count` INTEGER, `attributes` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "downloadLimitLimit", + "columnName": "download_limit_limit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "downloadLimitCount", + "columnName": "download_limit_count", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attributes", + "columnName": "attributes", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "offline_operations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `offline_operations_parent_oc_file_id` INTEGER, `offline_operations_path` TEXT, `offline_operations_type` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER, `offline_operations_modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "parentOCFileId", + "columnName": "offline_operations_parent_oc_file_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "path", + "columnName": "offline_operations_path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "offline_operations_type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filename", + "columnName": "offline_operations_file_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "offline_operations_created_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modifiedAt", + "columnName": "offline_operations_modified_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '93eb4d5fbf952984b6fc2df9f7c369e1')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/91.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/91.json new file mode 100644 index 0000000..a338fc4 --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/91.json @@ -0,0 +1,1195 @@ +{ + "formatVersion": 1, + "database": { + "version": 91, + "identityHash": "16f8a78a87d896adf01d545ed83142e5", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `forbidden_filename_characters` INTEGER, `forbidden_filenames` INTEGER, `forbidden_filename_extensions` INTEGER, `forbidden_filename_basenames` INTEGER, `files_download_limit` INTEGER, `files_download_limit_default` INTEGER, `recommendation` INTEGER, `notes_folder_path` TEXT, `default_permissions` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT" + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT" + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER" + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER" + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT" + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT" + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT" + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT" + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT" + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT" + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT" + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT" + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER" + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER" + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimit", + "columnName": "files_download_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimitDefault", + "columnName": "files_download_limit_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "INTEGER" + }, + { + "fieldPath": "notesFolderPath", + "columnName": "notes_folder_path", + "affinity": "TEXT" + }, + { + "fieldPath": "defaultPermissions", + "columnName": "default_permissions", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT, `uploaded` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT" + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT" + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER" + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER" + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT" + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER" + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT" + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER" + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER" + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER" + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER" + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER" + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT" + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT" + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT" + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT" + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT" + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT" + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER" + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT" + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT" + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT, `download_limit_limit` INTEGER, `download_limit_count` INTEGER, `attributes` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER" + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT" + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT" + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT" + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT" + }, + { + "fieldPath": "downloadLimitLimit", + "columnName": "download_limit_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "downloadLimitCount", + "columnName": "download_limit_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "attributes", + "columnName": "attributes", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER" + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER" + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER" + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER" + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "offline_operations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `offline_operations_parent_oc_file_id` INTEGER, `offline_operations_path` TEXT, `offline_operations_type` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER, `offline_operations_modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "parentOCFileId", + "columnName": "offline_operations_parent_oc_file_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "path", + "columnName": "offline_operations_path", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "offline_operations_type", + "affinity": "TEXT" + }, + { + "fieldPath": "filename", + "columnName": "offline_operations_file_name", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "offline_operations_created_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAt", + "columnName": "offline_operations_modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "recommended_files", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `directory` TEXT NOT NULL, `extension` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `has_preview` INTEGER NOT NULL, `reason` TEXT NOT NULL, `timestamp` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directory", + "columnName": "directory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "extension", + "columnName": "extension", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reason", + "columnName": "reason", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '16f8a78a87d896adf01d545ed83142e5')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/92.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/92.json new file mode 100644 index 0000000..092bf97 --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/92.json @@ -0,0 +1,1200 @@ +{ + "formatVersion": 1, + "database": { + "version": 92, + "identityHash": "aeef27ff00555d37d8605e760a446863", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `forbidden_filename_characters` INTEGER, `forbidden_filenames` INTEGER, `forbidden_filename_extensions` INTEGER, `forbidden_filename_basenames` INTEGER, `files_download_limit` INTEGER, `files_download_limit_default` INTEGER, `recommendation` INTEGER, `notes_folder_path` TEXT, `default_permissions` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT" + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT" + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER" + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER" + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT" + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT" + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT" + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT" + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT" + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT" + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT" + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT" + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER" + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER" + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimit", + "columnName": "files_download_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimitDefault", + "columnName": "files_download_limit_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "INTEGER" + }, + { + "fieldPath": "notesFolderPath", + "columnName": "notes_folder_path", + "affinity": "TEXT" + }, + { + "fieldPath": "defaultPermissions", + "columnName": "default_permissions", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT, `uploaded` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT" + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT" + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER" + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER" + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT" + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER" + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT" + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER" + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER" + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER" + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER" + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER" + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT" + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT" + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT" + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT" + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT" + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT" + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER" + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT" + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT" + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT, `download_limit_limit` INTEGER, `download_limit_count` INTEGER, `attributes` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER" + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT" + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT" + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT" + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT" + }, + { + "fieldPath": "downloadLimitLimit", + "columnName": "download_limit_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "downloadLimitCount", + "columnName": "download_limit_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "attributes", + "columnName": "attributes", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER" + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER" + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER" + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER" + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "offline_operations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `offline_operations_parent_oc_file_id` INTEGER, `offline_operations_path` TEXT, `offline_operations_type` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER, `offline_operations_modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "parentOCFileId", + "columnName": "offline_operations_parent_oc_file_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "path", + "columnName": "offline_operations_path", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "offline_operations_type", + "affinity": "TEXT" + }, + { + "fieldPath": "filename", + "columnName": "offline_operations_file_name", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "offline_operations_created_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAt", + "columnName": "offline_operations_modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "recommended_files", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `directory` TEXT NOT NULL, `extension` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `has_preview` INTEGER NOT NULL, `reason` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `account_name` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directory", + "columnName": "directory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "extension", + "columnName": "extension", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reason", + "columnName": "reason", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'aeef27ff00555d37d8605e760a446863')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/93.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/93.json new file mode 100644 index 0000000..c619e1b --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/93.json @@ -0,0 +1,1205 @@ +{ + "formatVersion": 1, + "database": { + "version": 93, + "identityHash": "bbaa274a7bcf9daf381451c8e77d6930", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `forbidden_filename_characters` INTEGER, `forbidden_filenames` INTEGER, `forbidden_filename_extensions` INTEGER, `forbidden_filename_basenames` INTEGER, `files_download_limit` INTEGER, `files_download_limit_default` INTEGER, `recommendation` INTEGER, `notes_folder_path` TEXT, `default_permissions` INTEGER, `user_status_supports_busy` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT" + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT" + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER" + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER" + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT" + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT" + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT" + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT" + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT" + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT" + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT" + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT" + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER" + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER" + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimit", + "columnName": "files_download_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimitDefault", + "columnName": "files_download_limit_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "INTEGER" + }, + { + "fieldPath": "notesFolderPath", + "columnName": "notes_folder_path", + "affinity": "TEXT" + }, + { + "fieldPath": "defaultPermissions", + "columnName": "default_permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsBusy", + "columnName": "user_status_supports_busy", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT, `uploaded` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT" + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT" + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER" + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER" + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT" + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER" + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT" + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER" + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER" + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER" + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER" + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER" + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT" + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT" + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT" + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT" + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT" + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT" + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER" + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT" + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT" + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT, `download_limit_limit` INTEGER, `download_limit_count` INTEGER, `attributes` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER" + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT" + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT" + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT" + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT" + }, + { + "fieldPath": "downloadLimitLimit", + "columnName": "download_limit_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "downloadLimitCount", + "columnName": "download_limit_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "attributes", + "columnName": "attributes", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER" + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER" + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER" + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER" + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "offline_operations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `offline_operations_parent_oc_file_id` INTEGER, `offline_operations_path` TEXT, `offline_operations_type` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER, `offline_operations_modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "parentOCFileId", + "columnName": "offline_operations_parent_oc_file_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "path", + "columnName": "offline_operations_path", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "offline_operations_type", + "affinity": "TEXT" + }, + { + "fieldPath": "filename", + "columnName": "offline_operations_file_name", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "offline_operations_created_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAt", + "columnName": "offline_operations_modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "recommended_files", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `directory` TEXT NOT NULL, `extension` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `has_preview` INTEGER NOT NULL, `reason` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `account_name` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directory", + "columnName": "directory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "extension", + "columnName": "extension", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reason", + "columnName": "reason", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bbaa274a7bcf9daf381451c8e77d6930')" + ] + } +} diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/94.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/94.json new file mode 100644 index 0000000..203afe3 --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/94.json @@ -0,0 +1,1210 @@ +{ + "formatVersion": 1, + "database": { + "version": 94, + "identityHash": "84d16467d3052f332b38942987052f00", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `forbidden_filename_characters` INTEGER, `forbidden_filenames` INTEGER, `forbidden_filename_extensions` INTEGER, `forbidden_filename_basenames` INTEGER, `files_download_limit` INTEGER, `files_download_limit_default` INTEGER, `recommendation` INTEGER, `notes_folder_path` TEXT, `default_permissions` INTEGER, `user_status_supports_busy` INTEGER, `windows_compatible_filenames` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT" + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT" + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER" + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER" + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT" + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT" + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT" + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT" + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT" + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT" + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT" + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT" + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER" + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER" + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimit", + "columnName": "files_download_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimitDefault", + "columnName": "files_download_limit_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "INTEGER" + }, + { + "fieldPath": "notesFolderPath", + "columnName": "notes_folder_path", + "affinity": "TEXT" + }, + { + "fieldPath": "defaultPermissions", + "columnName": "default_permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsBusy", + "columnName": "user_status_supports_busy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWCFEnabled", + "columnName": "windows_compatible_filenames", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT, `uploaded` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT" + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT" + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER" + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER" + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT" + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER" + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT" + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER" + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER" + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER" + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER" + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER" + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT" + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT" + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT" + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT" + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT" + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT" + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER" + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT" + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT" + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT, `download_limit_limit` INTEGER, `download_limit_count` INTEGER, `attributes` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER" + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT" + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT" + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT" + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT" + }, + { + "fieldPath": "downloadLimitLimit", + "columnName": "download_limit_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "downloadLimitCount", + "columnName": "download_limit_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "attributes", + "columnName": "attributes", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER" + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER" + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER" + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER" + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "offline_operations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `offline_operations_parent_oc_file_id` INTEGER, `offline_operations_path` TEXT, `offline_operations_type` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER, `offline_operations_modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "parentOCFileId", + "columnName": "offline_operations_parent_oc_file_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "path", + "columnName": "offline_operations_path", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "offline_operations_type", + "affinity": "TEXT" + }, + { + "fieldPath": "filename", + "columnName": "offline_operations_file_name", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "offline_operations_created_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAt", + "columnName": "offline_operations_modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "recommended_files", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `directory` TEXT NOT NULL, `extension` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `has_preview` INTEGER NOT NULL, `reason` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `account_name` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directory", + "columnName": "directory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "extension", + "columnName": "extension", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reason", + "columnName": "reason", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '84d16467d3052f332b38942987052f00')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/95.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/95.json new file mode 100644 index 0000000..694e08c --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/95.json @@ -0,0 +1,1215 @@ +{ + "formatVersion": 1, + "database": { + "version": 95, + "identityHash": "a2a95af369b3e75d48b6c454d1fe6c2d", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `forbidden_filename_characters` INTEGER, `forbidden_filenames` INTEGER, `forbidden_filename_extensions` INTEGER, `forbidden_filename_basenames` INTEGER, `files_download_limit` INTEGER, `files_download_limit_default` INTEGER, `recommendation` INTEGER, `notes_folder_path` TEXT, `default_permissions` INTEGER, `user_status_supports_busy` INTEGER, `windows_compatible_filenames` INTEGER, `has_valid_subscription` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT" + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT" + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER" + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER" + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT" + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT" + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT" + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT" + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT" + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT" + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT" + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT" + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER" + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER" + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimit", + "columnName": "files_download_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimitDefault", + "columnName": "files_download_limit_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "INTEGER" + }, + { + "fieldPath": "notesFolderPath", + "columnName": "notes_folder_path", + "affinity": "TEXT" + }, + { + "fieldPath": "defaultPermissions", + "columnName": "default_permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsBusy", + "columnName": "user_status_supports_busy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWCFEnabled", + "columnName": "windows_compatible_filenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasValidSubscription", + "columnName": "has_valid_subscription", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT, `uploaded` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT" + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT" + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER" + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER" + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT" + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER" + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT" + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER" + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER" + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER" + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER" + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER" + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT" + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT" + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT" + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT" + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT" + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT" + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER" + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT" + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT" + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT, `download_limit_limit` INTEGER, `download_limit_count` INTEGER, `attributes` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER" + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT" + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT" + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT" + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT" + }, + { + "fieldPath": "downloadLimitLimit", + "columnName": "download_limit_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "downloadLimitCount", + "columnName": "download_limit_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "attributes", + "columnName": "attributes", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER" + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER" + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER" + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER" + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "offline_operations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `offline_operations_parent_oc_file_id` INTEGER, `offline_operations_path` TEXT, `offline_operations_type` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER, `offline_operations_modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "parentOCFileId", + "columnName": "offline_operations_parent_oc_file_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "path", + "columnName": "offline_operations_path", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "offline_operations_type", + "affinity": "TEXT" + }, + { + "fieldPath": "filename", + "columnName": "offline_operations_file_name", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "offline_operations_created_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAt", + "columnName": "offline_operations_modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "recommended_files", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `directory` TEXT NOT NULL, `extension` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `has_preview` INTEGER NOT NULL, `reason` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `account_name` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directory", + "columnName": "directory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "extension", + "columnName": "extension", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reason", + "columnName": "reason", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a2a95af369b3e75d48b6c454d1fe6c2d')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/96.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/96.json new file mode 100644 index 0000000..4dc4849 --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/96.json @@ -0,0 +1,1293 @@ +{ + "formatVersion": 1, + "database": { + "version": 96, + "identityHash": "0e9718354266517a340a89e16bb7d373", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `forbidden_filename_characters` INTEGER, `forbidden_filenames` INTEGER, `forbidden_filename_extensions` INTEGER, `forbidden_filename_basenames` INTEGER, `files_download_limit` INTEGER, `files_download_limit_default` INTEGER, `recommendation` INTEGER, `notes_folder_path` TEXT, `default_permissions` INTEGER, `user_status_supports_busy` INTEGER, `windows_compatible_filenames` INTEGER, `has_valid_subscription` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT" + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT" + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER" + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER" + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT" + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT" + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT" + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT" + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT" + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT" + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT" + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT" + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER" + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER" + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimit", + "columnName": "files_download_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimitDefault", + "columnName": "files_download_limit_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "INTEGER" + }, + { + "fieldPath": "notesFolderPath", + "columnName": "notes_folder_path", + "affinity": "TEXT" + }, + { + "fieldPath": "defaultPermissions", + "columnName": "default_permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsBusy", + "columnName": "user_status_supports_busy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWCFEnabled", + "columnName": "windows_compatible_filenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasValidSubscription", + "columnName": "has_valid_subscription", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT, `uploaded` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT" + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT" + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER" + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER" + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT" + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER" + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT" + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER" + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER" + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER" + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER" + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER" + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT" + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT" + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT" + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT" + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT" + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT" + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER" + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT" + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT" + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT, `download_limit_limit` INTEGER, `download_limit_count` INTEGER, `attributes` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER" + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT" + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT" + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT" + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT" + }, + { + "fieldPath": "downloadLimitLimit", + "columnName": "download_limit_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "downloadLimitCount", + "columnName": "download_limit_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "attributes", + "columnName": "attributes", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER" + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER" + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER" + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER" + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "offline_operations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `offline_operations_parent_oc_file_id` INTEGER, `offline_operations_path` TEXT, `offline_operations_type` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER, `offline_operations_modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "parentOCFileId", + "columnName": "offline_operations_parent_oc_file_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "path", + "columnName": "offline_operations_path", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "offline_operations_type", + "affinity": "TEXT" + }, + { + "fieldPath": "filename", + "columnName": "offline_operations_file_name", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "offline_operations_created_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAt", + "columnName": "offline_operations_modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "recommended_files", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `directory` TEXT NOT NULL, `extension` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `has_preview` INTEGER NOT NULL, `reason` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `account_name` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directory", + "columnName": "directory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "extension", + "columnName": "extension", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reason", + "columnName": "reason", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "assistant", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT, `type` TEXT, `status` TEXT, `userId` TEXT, `appId` TEXT, `input` TEXT, `output` TEXT, `completionExpectedAt` INTEGER, `progress` INTEGER, `lastUpdated` INTEGER, `scheduledAt` INTEGER, `endedAt` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT" + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT" + }, + { + "fieldPath": "appId", + "columnName": "appId", + "affinity": "TEXT" + }, + { + "fieldPath": "input", + "columnName": "input", + "affinity": "TEXT" + }, + { + "fieldPath": "output", + "columnName": "output", + "affinity": "TEXT" + }, + { + "fieldPath": "completionExpectedAt", + "columnName": "completionExpectedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER" + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "endedAt", + "columnName": "endedAt", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0e9718354266517a340a89e16bb7d373')" + ] + } +} \ No newline at end of file diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_empty.png b/app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_empty.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_empty.png rename to app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_empty.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_empty_light_white.png b/app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_empty_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_empty_light_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_empty_light_white.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_error.png b/app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_error.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_error.png rename to app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_error.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_error_light_white.png b/app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_error_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_error_light_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_error_light_white.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_loading.png b/app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_loading.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_loading.png rename to app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_loading.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer.png b/app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer.png rename to app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer_dark_black.png b/app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer_dark_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer_dark_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer_dark_blue.png b/app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer_dark_blue.png rename to app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer_dark_white.png b/app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer_dark_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer_dark_white.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer_light_black.png b/app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer_light_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer_light_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer_light_white.png b/app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer_light_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_openDrawer_light_white.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_showActivities.png b/app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_showActivities.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_showActivities.png rename to app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_showActivities.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_showActivities_light_white.png b/app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_showActivities_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.ActivitiesActivityIT_showActivities_light_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.ActivitiesActivityIT_showActivities_light_white.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.AuthenticatorActivityIT_login.png b/app/screenshots/generic/debug/com.nextcloud.client.AuthenticatorActivityIT_login.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.AuthenticatorActivityIT_login.png rename to app/screenshots/generic/debug/com.nextcloud.client.AuthenticatorActivityIT_login.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.AuthenticatorActivityIT_login_dark_black.png b/app/screenshots/generic/debug/com.nextcloud.client.AuthenticatorActivityIT_login_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.AuthenticatorActivityIT_login_dark_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.AuthenticatorActivityIT_login_dark_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.AuthenticatorActivityIT_login_dark_blue.png b/app/screenshots/generic/debug/com.nextcloud.client.AuthenticatorActivityIT_login_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.AuthenticatorActivityIT_login_dark_blue.png rename to app/screenshots/generic/debug/com.nextcloud.client.AuthenticatorActivityIT_login_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.AuthenticatorActivityIT_login_dark_white.png b/app/screenshots/generic/debug/com.nextcloud.client.AuthenticatorActivityIT_login_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.AuthenticatorActivityIT_login_dark_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.AuthenticatorActivityIT_login_dark_white.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.AuthenticatorActivityIT_login_light_black.png b/app/screenshots/generic/debug/com.nextcloud.client.AuthenticatorActivityIT_login_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.AuthenticatorActivityIT_login_light_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.AuthenticatorActivityIT_login_light_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.AuthenticatorActivityIT_login_light_white.png b/app/screenshots/generic/debug/com.nextcloud.client.AuthenticatorActivityIT_login_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.AuthenticatorActivityIT_login_light_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.AuthenticatorActivityIT_login_light_white.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.CommunityActivityIT_open.png b/app/screenshots/generic/debug/com.nextcloud.client.CommunityActivityIT_open.png new file mode 100644 index 0000000..2b44c91 Binary files /dev/null and b/app/screenshots/generic/debug/com.nextcloud.client.CommunityActivityIT_open.png differ diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.CommunityActivityIT_open_dark_black.png b/app/screenshots/generic/debug/com.nextcloud.client.CommunityActivityIT_open_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.CommunityActivityIT_open_dark_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.CommunityActivityIT_open_dark_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.CommunityActivityIT_open_dark_blue.png b/app/screenshots/generic/debug/com.nextcloud.client.CommunityActivityIT_open_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.CommunityActivityIT_open_dark_blue.png rename to app/screenshots/generic/debug/com.nextcloud.client.CommunityActivityIT_open_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.CommunityActivityIT_open_dark_white.png b/app/screenshots/generic/debug/com.nextcloud.client.CommunityActivityIT_open_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.CommunityActivityIT_open_dark_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.CommunityActivityIT_open_dark_white.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.CommunityActivityIT_open_light_black.png b/app/screenshots/generic/debug/com.nextcloud.client.CommunityActivityIT_open_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.CommunityActivityIT_open_light_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.CommunityActivityIT_open_light_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.CommunityActivityIT_open_light_white.png b/app/screenshots/generic/debug/com.nextcloud.client.CommunityActivityIT_open_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.CommunityActivityIT_open_light_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.CommunityActivityIT_open_light_white.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityIT_shareToCircle.png b/app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityIT_shareToCircle.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityIT_shareToCircle.png rename to app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityIT_shareToCircle.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityIT_showAccounts.png b/app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityIT_showAccounts.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityIT_showAccounts.png rename to app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityIT_showAccounts.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityIT_showShares.png b/app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityIT_showShares.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityIT_showShares.png rename to app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityIT_showShares.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer.png b/app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer.png new file mode 100644 index 0000000..5c12e09 Binary files /dev/null and b/app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer.png differ diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer_dark_black.png b/app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer_dark_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer_dark_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer_dark_blue.png b/app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer_dark_blue.png rename to app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer_dark_white.png b/app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer_dark_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer_dark_white.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer_light_black.png b/app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer_light_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer_light_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer_light_white.png b/app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer_light_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer_light_white.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open.png b/app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open.png rename to app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open_dark_black.png b/app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open_dark_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open_dark_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open_dark_blue.png b/app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open_dark_blue.png rename to app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open_dark_white.png b/app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open_dark_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open_dark_white.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open_light_black.png b/app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open_light_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open_light_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open_light_white.png b/app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open_light_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_open_light_white.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_showMediaThenAllFiles.png b/app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_showMediaThenAllFiles.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_showMediaThenAllFiles.png rename to app/screenshots/generic/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_showMediaThenAllFiles.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FirstRunActivityIT_open.png b/app/screenshots/generic/debug/com.nextcloud.client.FirstRunActivityIT_open.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FirstRunActivityIT_open.png rename to app/screenshots/generic/debug/com.nextcloud.client.FirstRunActivityIT_open.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FirstRunActivityIT_open_dark_black.png b/app/screenshots/generic/debug/com.nextcloud.client.FirstRunActivityIT_open_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FirstRunActivityIT_open_dark_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.FirstRunActivityIT_open_dark_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FirstRunActivityIT_open_dark_blue.png b/app/screenshots/generic/debug/com.nextcloud.client.FirstRunActivityIT_open_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FirstRunActivityIT_open_dark_blue.png rename to app/screenshots/generic/debug/com.nextcloud.client.FirstRunActivityIT_open_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FirstRunActivityIT_open_dark_white.png b/app/screenshots/generic/debug/com.nextcloud.client.FirstRunActivityIT_open_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FirstRunActivityIT_open_dark_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.FirstRunActivityIT_open_dark_white.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FirstRunActivityIT_open_light_black.png b/app/screenshots/generic/debug/com.nextcloud.client.FirstRunActivityIT_open_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FirstRunActivityIT_open_light_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.FirstRunActivityIT_open_light_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FirstRunActivityIT_open_light_white.png b/app/screenshots/generic/debug/com.nextcloud.client.FirstRunActivityIT_open_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.FirstRunActivityIT_open_light_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.FirstRunActivityIT_open_light_white.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_open.png b/app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_open.png new file mode 100644 index 0000000..9015c83 Binary files /dev/null and b/app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_open.png differ diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_open_dark_black.png b/app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_open_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_open_dark_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_open_dark_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_open_dark_blue.png b/app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_open_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_open_dark_blue.png rename to app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_open_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_open_dark_white.png b/app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_open_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_open_dark_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_open_dark_white.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_open_light_black.png b/app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_open_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_open_light_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_open_light_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_open_light_white.png b/app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_open_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_open_light_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_open_light_white.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error.png b/app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error.png new file mode 100644 index 0000000..8a19c85 Binary files /dev/null and b/app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error.png differ diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error_dark_black.png b/app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error_dark_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error_dark_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error_dark_blue.png b/app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error_dark_blue.png rename to app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error_dark_white.png b/app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error_dark_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error_dark_white.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error_light_black.png b/app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error_light_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error_light_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error_light_white.png b/app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error_light_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error_light_white.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open.png b/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open.png new file mode 100644 index 0000000..ec519bc Binary files /dev/null and b/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open.png differ diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer.png b/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer.png rename to app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer_dark_blue.png b/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer_dark_blue.png rename to app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer_dark_white.png b/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer_dark_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer_dark_white.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer_light_white.png b/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer_light_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_openDrawer_light_white.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_dark_black.png b/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_dark_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_dark_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_dark_blue.png b/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_dark_blue.png rename to app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_dark_white.png b/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_dark_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_dark_white.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_light_black.png b/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_light_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_light_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_light_white.png b/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_light_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_open_light_white.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_showPowerCheckDialog.png b/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_showPowerCheckDialog.png new file mode 100644 index 0000000..c99e664 Binary files /dev/null and b/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_showPowerCheckDialog.png differ diff --git a/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog.png b/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog.png new file mode 100644 index 0000000..99e5eb9 Binary files /dev/null and b/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog.png differ diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_dark_black.png b/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_dark_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_dark_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_dark_blue.png b/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_dark_blue.png rename to app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_dark_white.png b/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_dark_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_dark_white.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_light_black.png b/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_light_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_light_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_light_white.png b/app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_light_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog_light_white.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer.png b/app/screenshots/generic/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer.png new file mode 100644 index 0000000..0d8d7ed Binary files /dev/null and b/app/screenshots/generic/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer.png differ diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer_dark_black.png b/app/screenshots/generic/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer_dark_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer_dark_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer_dark_blue.png b/app/screenshots/generic/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer_dark_blue.png rename to app/screenshots/generic/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer_dark_white.png b/app/screenshots/generic/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer_dark_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer_dark_white.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer_light_black.png b/app/screenshots/generic/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer_light_black.png rename to app/screenshots/generic/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer_light_black.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer_light_white.png b/app/screenshots/generic/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer_light_white.png rename to app/screenshots/generic/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer_light_white.png diff --git a/app/screenshots/generic/debug/com.nextcloud.client.etm.EtmActivityTest_accounts.png b/app/screenshots/generic/debug/com.nextcloud.client.etm.EtmActivityTest_accounts.png new file mode 100644 index 0000000..555ae8f Binary files /dev/null and b/app/screenshots/generic/debug/com.nextcloud.client.etm.EtmActivityTest_accounts.png differ diff --git a/app/screenshots/generic/debug/com.nextcloud.client.etm.EtmActivityTest_overview.png b/app/screenshots/generic/debug/com.nextcloud.client.etm.EtmActivityTest_overview.png new file mode 100644 index 0000000..774de81 Binary files /dev/null and b/app/screenshots/generic/debug/com.nextcloud.client.etm.EtmActivityTest_overview.png differ diff --git a/app/screenshots/gplay/debug/com.nextcloud.ui.BitmapIT_glideSVG.png b/app/screenshots/generic/debug/com.nextcloud.ui.BitmapIT_glideSVG.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.ui.BitmapIT_glideSVG.png rename to app/screenshots/generic/debug/com.nextcloud.ui.BitmapIT_glideSVG.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.ui.BitmapIT_roundBitmap.png b/app/screenshots/generic/debug/com.nextcloud.ui.BitmapIT_roundBitmap.png similarity index 100% rename from app/screenshots/gplay/debug/com.nextcloud.ui.BitmapIT_roundBitmap.png rename to app/screenshots/generic/debug/com.nextcloud.ui.BitmapIT_roundBitmap.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth.png new file mode 100644 index 0000000..e276971 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting.png new file mode 100644 index 0000000..b03c71c Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew.png new file mode 100644 index 0000000..08bfbf1 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles.png new file mode 100644 index 0000000..e5b402d Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference.png new file mode 100644 index 0000000..a3e76e7 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF.png new file mode 100644 index 0000000..bd17b40 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open.png new file mode 100644 index 0000000..86f6341 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_testChooseLocationAction.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_testChooseLocationAction.png new file mode 100644 index 0000000..ffd12d3 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_testChooseLocationAction.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_testMoveOrCopy.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_testMoveOrCopy.png new file mode 100644 index 0000000..25098f2 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_testMoveOrCopy.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open.png new file mode 100644 index 0000000..b32e258 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail.png new file mode 100644 index 0000000..6b8b3d3 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_empty_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_error_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.NotificationsActivityIT_showNotifications_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ReceiveExternalFilesActivityIT_open.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ReceiveExternalFilesActivityIT_open.png new file mode 100644 index 0000000..dd22ad1 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ReceiveExternalFilesActivityIT_open.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ReceiveExternalFilesActivityIT_openMultiAccount.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ReceiveExternalFilesActivityIT_openMultiAccount.png new file mode 100644 index 0000000..417d65b Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.ReceiveExternalFilesActivityIT_openMultiAccount.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_localFolderPickerMode.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_localFolderPickerMode.png new file mode 100644 index 0000000..a463f86 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_localFolderPickerMode.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_noneSelected.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_noneSelected.png new file mode 100644 index 0000000..97ec133 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_noneSelected.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_open.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_open.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_open.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_open.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_search.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_search.png new file mode 100644 index 0000000..a8960fa Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_search.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_selectAll.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_selectAll.png new file mode 100644 index 0000000..62949bb Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_selectAll.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail.png new file mode 100644 index 0000000..db6f80f Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog.png new file mode 100644 index 0000000..a3db7ab Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled.png new file mode 100644 index 0000000..b1ba5c6 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away.png new file mode 100644 index 0000000..695e7fc Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dark_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd.png new file mode 100644 index 0000000..2f551cf Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun.png new file mode 100644 index 0000000..74df5cc Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline.png new file mode 100644 index 0000000..a3db7ab Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online.png new file mode 100644 index 0000000..47a365d Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet.png new file mode 100644 index 0000000..8e618f1 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithOneAction.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithOneAction.png new file mode 100644 index 0000000..6c51e11 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithOneAction.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeAction.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeAction.png new file mode 100644 index 0000000..bbadc48 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeAction.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeActionRTL.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeActionRTL.png new file mode 100644 index 0000000..8366a9d Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeActionRTL.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithTwoAction.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithTwoAction.png new file mode 100644 index 0000000..0a1d6b9 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithTwoAction.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testEnforcedPasswordDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testEnforcedPasswordDialog.png new file mode 100644 index 0000000..0045908 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testEnforcedPasswordDialog.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png new file mode 100644 index 0000000..de41df9 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog.png new file mode 100644 index 0000000..126a08f Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog.png new file mode 100644 index 0000000..941ef2a Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testOptionalPasswordDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testOptionalPasswordDialog.png new file mode 100644 index 0000000..3b12b3e Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testOptionalPasswordDialog.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testProfileBottomSheet.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testProfileBottomSheet.png new file mode 100644 index 0000000..004bb41 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testProfileBottomSheet.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog.png new file mode 100644 index 0000000..1d8bf52 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog.png new file mode 100644 index 0000000..8feba86 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog.png new file mode 100644 index 0000000..0f890be Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog.png new file mode 100644 index 0000000..8feba86 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog.png new file mode 100644 index 0000000..8705ea8 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testSslUntrustedCertDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testSslUntrustedCertDialog.png new file mode 100644 index 0000000..24b3f52 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testSslUntrustedCertDialog.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testStoragePermissionDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testStoragePermissionDialog.png new file mode 100644 index 0000000..7e4b3f5 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testStoragePermissionDialog.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testAccountChooserDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testAccountChooserDialog.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testAccountChooserDialog.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testAccountChooserDialog.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testLoadingDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testLoadingDialog.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testLoadingDialog.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testLoadingDialog.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testNewFolderDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testNewFolderDialog.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testNewFolderDialog.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testNewFolderDialog.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testRemoveFileDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testRemoveFileDialog.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testRemoveFileDialog.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testRemoveFileDialog.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testRemoveFilesDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testRemoveFilesDialog.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testRemoveFilesDialog.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testRemoveFilesDialog.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testRemoveFolderDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testRemoveFolderDialog.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testRemoveFolderDialog.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testRemoveFolderDialog.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testRemoveFoldersDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testRemoveFoldersDialog.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testRemoveFoldersDialog.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testRemoveFoldersDialog.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testRenameFileDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testRenameFileDialog.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testRenameFileDialog.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.DialogFragmentTest_testRenameFileDialog.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialogDifferentTypes_Screenshot.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialogDifferentTypes_Screenshot.png new file mode 100644 index 0000000..fcec93d Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialogDifferentTypes_Screenshot.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_Screenshot.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_Screenshot.png new file mode 100644 index 0000000..fcec93d Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_Screenshot.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog.png new file mode 100644 index 0000000..2526f31 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error.png new file mode 100644 index 0000000..519a098 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic.png new file mode 100644 index 0000000..1ad442c Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragmentTest_showNotEnoughSpaceDialogForFile.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragmentTest_showNotEnoughSpaceDialogForFile.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragmentTest_showNotEnoughSpaceDialogForFile.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragmentTest_showNotEnoughSpaceDialogForFile.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragmentTest_showNotEnoughSpaceDialogForFolder.png b/app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragmentTest_showNotEnoughSpaceDialogForFolder.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragmentTest_showNotEnoughSpaceDialogForFolder.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragmentTest_showNotEnoughSpaceDialogForFolder.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatarsWithStatus_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.AvatarIT_showAvatars_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showCalendarAndContactsList.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showCalendarAndContactsList.png new file mode 100644 index 0000000..9343c4f Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showCalendarAndContactsList.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showCalendarList.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showCalendarList.png new file mode 100644 index 0000000..7dc05ca Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showCalendarList.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showContactList.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showContactList.png new file mode 100644 index 0000000..1531ddd Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showContactList.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showLoading.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showLoading.png new file mode 100644 index 0000000..bd17b40 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showLoading.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.ContactListFragmentIT_showContactListFragmentLoading_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesError_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivitiesNone_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsActivities_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetailsSharing_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetails_Activities.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetails_Activities.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetails_Activities.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetails_Activities.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetails_Sharing.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetails_Sharing.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetails_Sharing.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showDetails_Sharing.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailActivitiesFragment_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailDetailsFragment.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailDetailsFragment.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailDetailsFragment.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailDetailsFragment.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT_showFileDetailSharingFragment_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesDownloadLimit.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesDownloadLimit.png new file mode 100644 index 0000000..ac96f0c Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesDownloadLimit.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes.png new file mode 100644 index 0000000..cc50640 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone.png new file mode 100644 index 0000000..365a99e Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed.png new file mode 100644 index 0000000..341b02c Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listShares_file_allShareTypes.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listShares_file_allShareTypes.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listShares_file_allShareTypes.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listShares_file_allShareTypes.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listShares_file_none.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listShares_file_none.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listShares_file_none.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listShares_file_none.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listShares_file_resharing_not_allowed.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listShares_file_resharing_not_allowed.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listShares_file_resharing_not_allowed.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listShares_file_resharing_not_allowed.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_publicLink_optionMenu.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_publicLink_optionMenu.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_publicLink_optionMenu.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_publicLink_optionMenu.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.GalleryFragmentIT_showEmpty.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.GalleryFragmentIT_showEmpty.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.GalleryFragmentIT_showEmpty.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.GalleryFragmentIT_showEmpty.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.GalleryFragmentIT_showGallery.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.GalleryFragmentIT_showGallery.png new file mode 100644 index 0000000..7a45f94 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.GalleryFragmentIT_showGallery.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.GroupfolderListFragmentIT_showEmpty.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.GroupfolderListFragmentIT_showEmpty.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.GroupfolderListFragmentIT_showEmpty.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.GroupfolderListFragmentIT_showEmpty.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.GroupfolderListFragmentIT_showGroupfolder.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.GroupfolderListFragmentIT_showGroupfolder.png new file mode 100644 index 0000000..1938997 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.GroupfolderListFragmentIT_showGroupfolder.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.GroupfolderListFragmentIT_showGroupfolders.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.GroupfolderListFragmentIT_showGroupfolders.png new file mode 100644 index 0000000..939279d Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.GroupfolderListFragmentIT_showGroupfolders.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentIT_createAndShowShareToCircle.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentIT_createAndShowShareToCircle.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentIT_createAndShowShareToCircle.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentIT_createAndShowShareToCircle.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentIT_createAndShowShareToGroup.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentIT_createAndShowShareToGroup.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentIT_createAndShowShareToGroup.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentIT_createAndShowShareToGroup.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentIT_createAndShowShareToUser.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentIT_createAndShowShareToUser.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentIT_createAndShowShareToUser.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentIT_createAndShowShareToUser.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentIT_createAndShowShareViaLink.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentIT_createAndShowShareViaLink.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentIT_createAndShowShareViaLink.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentIT_createAndShowShareViaLink.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles.png new file mode 100644 index 0000000..7e58a22 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFolderTypes.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFolderTypes.png new file mode 100644 index 0000000..e00c58c Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFolderTypes.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showOneFile.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showOneFile.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showOneFile.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showOneFile.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace.png new file mode 100644 index 0000000..c8abed0 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles.png new file mode 100644 index 0000000..8f9e17c Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles_light_white.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.SharedListFragmentIT_showSharedFiles.png b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.SharedListFragmentIT_showSharedFiles.png new file mode 100644 index 0000000..4963d53 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.fragment.SharedListFragmentIT_showSharedFiles.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.preview.PreviewBitmapScreenshotIT_showBitmap.png b/app/screenshots/generic/debug/com.owncloud.android.ui.preview.PreviewBitmapScreenshotIT_showBitmap.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.preview.PreviewBitmapScreenshotIT_showBitmap.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.preview.PreviewBitmapScreenshotIT_showBitmap.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.preview.PreviewImageFragmentIT_corruptImage.png b/app/screenshots/generic/debug/com.owncloud.android.ui.preview.PreviewImageFragmentIT_corruptImage.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.preview.PreviewImageFragmentIT_corruptImage.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.preview.PreviewImageFragmentIT_corruptImage.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.preview.PreviewImageFragmentIT_corruptImage_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.preview.PreviewImageFragmentIT_corruptImage_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.preview.PreviewImageFragmentIT_corruptImage_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.preview.PreviewImageFragmentIT_corruptImage_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.preview.PreviewImageFragmentIT_validImage.png b/app/screenshots/generic/debug/com.owncloud.android.ui.preview.PreviewImageFragmentIT_validImage.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.preview.PreviewImageFragmentIT_validImage.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.preview.PreviewImageFragmentIT_validImage.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.preview.PreviewImageFragmentIT_validImage_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.preview.PreviewImageFragmentIT_validImage_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.preview.PreviewImageFragmentIT_validImage_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.preview.PreviewImageFragmentIT_validImage_dark_blue.png diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.preview.PreviewTextFileFragmentTest_displayJavaSnippetFile.png b/app/screenshots/generic/debug/com.owncloud.android.ui.preview.PreviewTextFileFragmentTest_displayJavaSnippetFile.png new file mode 100644 index 0000000..3b28659 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.preview.PreviewTextFileFragmentTest_displayJavaSnippetFile.png differ diff --git a/app/screenshots/generic/debug/com.owncloud.android.ui.preview.PreviewTextFileFragmentTest_displaySimpleTextFile.png b/app/screenshots/generic/debug/com.owncloud.android.ui.preview.PreviewTextFileFragmentTest_displaySimpleTextFile.png new file mode 100644 index 0000000..22c0729 Binary files /dev/null and b/app/screenshots/generic/debug/com.owncloud.android.ui.preview.PreviewTextFileFragmentTest_displaySimpleTextFile.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.preview.pdf.PreviewPdfFragmentScreenshotIT_showPdf.png b/app/screenshots/generic/debug/com.owncloud.android.ui.preview.pdf.PreviewPdfFragmentScreenshotIT_showPdf.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.preview.pdf.PreviewPdfFragmentScreenshotIT_showPdf.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.preview.pdf.PreviewPdfFragmentScreenshotIT_showPdf.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_differentUser.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_differentUser.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_differentUser.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_differentUser.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_empty_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_error_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_files_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading_dark_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading_dark_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading_dark_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading_dark_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading_dark_blue.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading_dark_blue.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading_dark_blue.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading_dark_blue.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading_dark_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading_dark_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading_dark_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading_dark_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading_light_black.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading_light_black.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading_light_black.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading_light_black.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading_light_white.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading_light_white.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading_light_white.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_loading_light_white.png diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_normalUser.png b/app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_normalUser.png similarity index 100% rename from app/screenshots/gplay/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_normalUser.png rename to app/screenshots/generic/debug/com.owncloud.android.ui.trashbin.TrashbinActivityIT_normalUser.png diff --git a/app/screenshots/gplay/debug/richworkspaces_dark.png b/app/screenshots/generic/debug/richworkspaces_dark.png similarity index 100% rename from app/screenshots/gplay/debug/richworkspaces_dark.png rename to app/screenshots/generic/debug/richworkspaces_dark.png diff --git a/app/screenshots/gplay/debug/richworkspaces_light.png b/app/screenshots/generic/debug/richworkspaces_light.png similarity index 100% rename from app/screenshots/gplay/debug/richworkspaces_light.png rename to app/screenshots/generic/debug/richworkspaces_light.png diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.CommunityActivityIT_open.png b/app/screenshots/gplay/debug/com.nextcloud.client.CommunityActivityIT_open.png deleted file mode 100644 index 912c0b5..0000000 Binary files a/app/screenshots/gplay/debug/com.nextcloud.client.CommunityActivityIT_open.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer.png b/app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer.png deleted file mode 100644 index d980721..0000000 Binary files a/app/screenshots/gplay/debug/com.nextcloud.client.FileDisplayActivityScreenshotIT_drawer.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_open.png b/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_open.png deleted file mode 100644 index a5159c8..0000000 Binary files a/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_open.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error.png b/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error.png deleted file mode 100644 index f243297..0000000 Binary files a/app/screenshots/gplay/debug/com.nextcloud.client.SettingsActivityIT_showMnemonic_Error.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_open.png b/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_open.png deleted file mode 100644 index 562481f..0000000 Binary files a/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_open.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog.png b/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog.png deleted file mode 100644 index 272d022..0000000 Binary files a/app/screenshots/gplay/debug/com.nextcloud.client.SyncedFoldersActivityIT_testSyncedFolderDialog.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer.png b/app/screenshots/gplay/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer.png deleted file mode 100644 index 10d5bac..0000000 Binary files a/app/screenshots/gplay/debug/com.nextcloud.client.UploadListActivityActivityIT_openDrawer.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.etm.EtmActivityTest_accounts.png b/app/screenshots/gplay/debug/com.nextcloud.client.etm.EtmActivityTest_accounts.png deleted file mode 100644 index f4dc216..0000000 Binary files a/app/screenshots/gplay/debug/com.nextcloud.client.etm.EtmActivityTest_accounts.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.nextcloud.client.etm.EtmActivityTest_overview.png b/app/screenshots/gplay/debug/com.nextcloud.client.etm.EtmActivityTest_overview.png deleted file mode 100644 index d710906..0000000 Binary files a/app/screenshots/gplay/debug/com.nextcloud.client.etm.EtmActivityTest_overview.png and /dev/null differ 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 deleted file mode 100644 index d73ab84..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth.png and /dev/null 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 deleted file mode 100644 index 11aa236..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting.png and /dev/null 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 deleted file mode 100644 index ce379cf..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew.png and /dev/null 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 deleted file mode 100644 index fa9a10c..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles.png and /dev/null 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 deleted file mode 100644 index 720e76b..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openContactsPreference.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF.png deleted file mode 100644 index eadc8cd..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ContactsPreferenceActivityIT_openVCF.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open.png deleted file mode 100644 index 37ca6ec..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_open.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_testChooseLocationAction.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_testChooseLocationAction.png deleted file mode 100644 index e49c41b..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_testChooseLocationAction.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_testMoveOrCopy.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_testMoveOrCopy.png deleted file mode 100644 index fe3726f..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.FolderPickerActivityIT_testMoveOrCopy.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open.png deleted file mode 100644 index 36cbbc4..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_open.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail.png deleted file mode 100644 index df06b48..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ManageAccountsActivityIT_userInfoDetail.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ReceiveExternalFilesActivityIT_open.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ReceiveExternalFilesActivityIT_open.png deleted file mode 100644 index 1bb7b1f..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ReceiveExternalFilesActivityIT_open.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ReceiveExternalFilesActivityIT_openMultiAccount.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ReceiveExternalFilesActivityIT_openMultiAccount.png deleted file mode 100644 index 4af8e9a..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.ReceiveExternalFilesActivityIT_openMultiAccount.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_localFolderPickerMode.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_localFolderPickerMode.png deleted file mode 100644 index a029978..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_localFolderPickerMode.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_noneSelected.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_noneSelected.png deleted file mode 100644 index 3e5fbdf..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UploadFilesActivityIT_noneSelected.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail.png deleted file mode 100644 index 3335880..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.UserInfoActivityIT_fullUserInfoDetail.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog.png deleted file mode 100644 index de21099..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled.png deleted file mode 100644 index 0f0607c..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialogWithStatusDisabled.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away.png deleted file mode 100644 index 971dd17..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_away.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd.png deleted file mode 100644 index 4ce6435..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_dnd.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun.png deleted file mode 100644 index 6e5ea2a..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_fun.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline.png deleted file mode 100644 index de21099..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_offline.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online.png deleted file mode 100644 index f66a7ee..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testAccountChooserDialog_online.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet.png deleted file mode 100644 index 0b64a27..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testBottomSheet.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithOneAction.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithOneAction.png deleted file mode 100644 index 3977140..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithOneAction.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeAction.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeAction.png deleted file mode 100644 index 1ffb0f9..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeAction.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeActionRTL.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeActionRTL.png deleted file mode 100644 index 365aed3..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithThreeActionRTL.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithTwoAction.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithTwoAction.png deleted file mode 100644 index e9fd77f..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testConfirmationDialogWithTwoAction.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testEnforcedPasswordDialog.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testEnforcedPasswordDialog.png deleted file mode 100644 index 815f93d..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testEnforcedPasswordDialog.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png deleted file mode 100644 index 72d4ba3..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testFileActionsBottomSheet.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog.png deleted file mode 100644 index 237dcd1..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testLoadingDialog.png and /dev/null 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 deleted file mode 100644 index d4dfd20..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testNewFolderDialog.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testOptionalPasswordDialog.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testOptionalPasswordDialog.png deleted file mode 100644 index 69859a4..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testOptionalPasswordDialog.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testProfileBottomSheet.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testProfileBottomSheet.png deleted file mode 100644 index 1c57c3c..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testProfileBottomSheet.png and /dev/null 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 deleted file mode 100644 index cce676c..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFileDialog.png and /dev/null 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 deleted file mode 100644 index 5e73170..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFilesDialog.png and /dev/null 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 deleted file mode 100644 index 9a367f2..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFolderDialog.png and /dev/null 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 deleted file mode 100644 index 5e73170..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRemoveFoldersDialog.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog.png deleted file mode 100644 index 18f8e02..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testRenameFileDialog.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testSslUntrustedCertDialog.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testSslUntrustedCertDialog.png deleted file mode 100644 index 655039e..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testSslUntrustedCertDialog.png and /dev/null 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 deleted file mode 100644 index c0b1971..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.DialogFragmentIT_testStoragePermissionDialog.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialogDifferentTypes_Screenshot.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialogDifferentTypes_Screenshot.png deleted file mode 100644 index f8d7757..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialogDifferentTypes_Screenshot.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_Screenshot.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_Screenshot.png deleted file mode 100644 index f8d7757..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendFilesDialogTest_showDialog_Screenshot.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog.png deleted file mode 100644 index 188f61c..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SendShareDialogTest_showDialog.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error.png deleted file mode 100644 index 2ced2a1..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_error.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic.png deleted file mode 100644 index 10c94a3..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT_showMnemonic.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showCalendarAndContactsList.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showCalendarAndContactsList.png deleted file mode 100644 index 84625b4..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showCalendarAndContactsList.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showCalendarList.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showCalendarList.png deleted file mode 100644 index 568024b..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showCalendarList.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showContactList.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showContactList.png deleted file mode 100644 index d9a3848..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showContactList.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showLoading.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showLoading.png deleted file mode 100644 index eadc8cd..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.BackupListFragmentIT_showLoading.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes.png deleted file mode 100644 index 21818eb..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileAllShareTypes.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone.png deleted file mode 100644 index 9345a24..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileNone.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed.png deleted file mode 100644 index 40f2398..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT_listSharesFileResharingNotAllowed.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.GalleryFragmentIT_showGallery.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.GalleryFragmentIT_showGallery.png deleted file mode 100644 index 7998931..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.GalleryFragmentIT_showGallery.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.GroupfolderListFragmentIT_showGroupfolder.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.GroupfolderListFragmentIT_showGroupfolder.png deleted file mode 100644 index 12f3388..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.GroupfolderListFragmentIT_showGroupfolder.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.GroupfolderListFragmentIT_showGroupfolders.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.GroupfolderListFragmentIT_showGroupfolders.png deleted file mode 100644 index cdc2a72..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.GroupfolderListFragmentIT_showGroupfolders.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles.png deleted file mode 100644 index b912411..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFiles.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFolderTypes.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFolderTypes.png deleted file mode 100644 index 6befb0a..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showFolderTypes.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace.png deleted file mode 100644 index 5579f8b..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showRichWorkspace.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles.png deleted file mode 100644 index 9082d3e..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT_showSharedFiles.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.SharedListFragmentIT_showSharedFiles.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.SharedListFragmentIT_showSharedFiles.png deleted file mode 100644 index 37662fd..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.fragment.SharedListFragmentIT_showSharedFiles.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.preview.PreviewTextFileFragmentTest_displayJavaSnippetFile.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.preview.PreviewTextFileFragmentTest_displayJavaSnippetFile.png deleted file mode 100644 index 4b8e5ba..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.preview.PreviewTextFileFragmentTest_displayJavaSnippetFile.png and /dev/null differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.preview.PreviewTextFileFragmentTest_displaySimpleTextFile.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.preview.PreviewTextFileFragmentTest_displaySimpleTextFile.png deleted file mode 100644 index bba71fe..0000000 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.preview.PreviewTextFileFragmentTest_displaySimpleTextFile.png and /dev/null differ diff --git a/app/src/androidTest/AndroidManifest.xml b/app/src/androidTest/AndroidManifest.xml index 74ade86..7544109 100644 --- a/app/src/androidTest/AndroidManifest.xml +++ b/app/src/androidTest/AndroidManifest.xml @@ -4,7 +4,7 @@ ~ ~ SPDX-FileCopyrightText: 2020-2024 Nextcloud GmbH and Nextcloud contributors ~ SPDX-FileCopyrightText: 2020 Tobias Kaminsky - ~ SPDX-License-Identifier: AGPL-3.0-or-later + ~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only ~ ~ UI Automator requires Android 4.3 (API level 18) or higher. ~ So this AndroidManifest will be merged with the normal one (not requiring to change the minSdk) diff --git a/app/src/androidTest/assets/credentials.json b/app/src/androidTest/assets/credentials.json new file mode 100644 index 0000000..8e66049 --- /dev/null +++ b/app/src/androidTest/assets/credentials.json @@ -0,0 +1,4 @@ +{ + "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3wSNveXIhRsKl86pUnL7\n/AIAH+IJya5vqP0lv+yCBkd728szrLRYRWxPNC4VDbzyRHBr0RWj0ibsLJvU2OeF\n5p4er1tMIGgB0AEwiuDXBBz/RrxjPdhlilq7mvvqeUS2M3t5iroIxM6VEGQrhVrb\nb3U+7c6Lt7dIHAHEVOXnZiHYhhhduEmIzbsrAZFuMjlnWXTiMhuuWBf6t1nPyCHa\noA96loWibbvIsMegC73J3Ej5sgLkz/TjlrYmv6p3RGAEs74KHfggy4Fzw9TxBAAY\nyIX0NY8Rhb10XKrOSXrvRYuL/wkJ3P5XVK/NfsuLKbrhuUjDSgKplY9xCtOSaEPJ\nVQIDAQAB\n-----END PUBLIC KEY-----", + "certificate": "-----BEGIN CERTIFICATE-----\nMIIC9DCCAdygAwIBAgIBADANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDDAhuYXJy\nYXRvcjAeFw0yNDA1MjcxMzEyNDVaFw00NDA1MjIxMzEyNDVaMBMxETAPBgNVBAMM\nCG5hcnJhdG9yMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjA2EEYeN\nc3BdVDPkJK/AWPB1kd9sWAonZt/V4sbAE6fGy4qU21xfInZQaMHyhqdMXga10juE\nJLPKuyyRz+qijASryW+WzCJ3A9QeHHO+CiLc09yuB80JRpH0oHsol6WrdO1n5zuH\nlPtAdCwi4OeRmvazfBysbP2gaUl7DxackqbMei8a0MoyDxUB11hp0tpyYAU1/sXZ\nLGh4R4q4/F2KlSeYY9D62OJ8wNTgv9AYF/HRxXxWmVftB1En/DdvVr1zJGraHiRm\nQbaEnmsSGK8QHHm4h37cfD5f7rW1WO5A8KyJKwluOIXjMfL1YijAPpNW6EHhSlfT\n5RVLCHxvrzMHewIDAQABo1MwUTAdBgNVHQ4EFgQUzT6RHEHtpdjr8N3ABJK0wpFt\n1PMwHwYDVR0jBBgwFoAUzT6RHEHtpdjr8N3ABJK0wpFt1PMwDwYDVR0TAQH/BAUw\nAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAJ1q3CSBHrLauOZAD56BeElgh/ahbegsE\nZ4w7q4FdhkixLIwe6yrMmSvpNTuxRDHUrVLXxQmN0X3Yb7BLNXnnIUfH9EozaV7p\nYjOLWD2XCfLJmpGIBVvqZhyZrTl69jkBaVHF78aj1vt+qKihHUAVnG+qGH0PFms+\nG0KyY8bNYg+2HQiSTva1kgGPUA/8nQNj3lwi+r03tgqbw88fQKRPeMUJWdh/yV9U\noBdPHt+TBsUFZQZP3lBBS9lYhDT9fNoGX12WPAEUjYNhHVX+Qdup8Mg3aUMITXXJ\nvlGsN1SknlLoN0RwBFbyH9BCzqAdEIj5qQM3YDzIIyyy6AAnswNEUg==\n-----END CERTIFICATE-----" +} diff --git a/app/src/androidTest/disabledTests/uiautomator/InitialTest.java b/app/src/androidTest/disabledTests/uiautomator/InitialTest.java index 6f28594..b548770 100644 --- a/app/src/androidTest/disabledTests/uiautomator/InitialTest.java +++ b/app/src/androidTest/disabledTests/uiautomator/InitialTest.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2018 Tobias Kaminsky * SPDX-FileCopyrightText: 2015 ownCloud Inc. - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.uiautomator; diff --git a/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt b/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt index 1d8bb85..8c3157c 100644 --- a/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt +++ b/app/src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client diff --git a/app/src/androidTest/java/com/nextcloud/client/AuthenticatorActivityIT.java b/app/src/androidTest/java/com/nextcloud/client/AuthenticatorActivityIT.java index 4bd1121..4c704f0 100644 --- a/app/src/androidTest/java/com/nextcloud/client/AuthenticatorActivityIT.java +++ b/app/src/androidTest/java/com/nextcloud/client/AuthenticatorActivityIT.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2021 Andy Scherzinger - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client; diff --git a/app/src/androidTest/java/com/nextcloud/client/CommunityActivityIT.java b/app/src/androidTest/java/com/nextcloud/client/CommunityActivityIT.java deleted file mode 100644 index 2fbf002..0000000 --- a/app/src/androidTest/java/com/nextcloud/client/CommunityActivityIT.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2019 Tobias Kaminsky - * SPDX-FileCopyrightText: 2019 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.nextcloud.client; - -import android.app.Activity; - -import com.nextcloud.test.GrantStoragePermissionRule; -import com.owncloud.android.AbstractIT; -import com.owncloud.android.ui.activity.CommunityActivity; -import com.owncloud.android.utils.ScreenshotTest; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TestRule; - -import androidx.test.espresso.intent.rule.IntentsTestRule; - - -public class CommunityActivityIT extends AbstractIT { - @Rule public IntentsTestRule activityRule = new IntentsTestRule<>(CommunityActivity.class, - true, - false); - - @Rule - public final TestRule permissionRule = GrantStoragePermissionRule.grant(); - - @Test - @ScreenshotTest - public void open() { - Activity sut = activityRule.launchActivity(null); - - screenshot(sut); - } -} diff --git a/app/src/androidTest/java/com/nextcloud/client/CommunityActivityIT.kt b/app/src/androidTest/java/com/nextcloud/client/CommunityActivityIT.kt new file mode 100644 index 0000000..d44962b --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/client/CommunityActivityIT.kt @@ -0,0 +1,59 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-FileCopyrightText: 2019 Tobias Kaminsky + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.nextcloud.client + +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import com.nextcloud.test.GrantStoragePermissionRule.Companion.grant +import com.owncloud.android.AbstractIT +import com.owncloud.android.ui.activity.CommunityActivity +import com.owncloud.android.utils.EspressoIdlingResource +import com.owncloud.android.utils.ScreenshotTest +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule + +class CommunityActivityIT : AbstractIT() { + private val testClassName = "com.nextcloud.client.CommunityActivityIT" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } + + @get:Rule + var storagePermissionRule: TestRule = grant() + + @Test + @UiThread + @ScreenshotTest + fun open() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + val screenShotName = createName(testClassName + "_" + "open", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } + } +} diff --git a/app/src/androidTest/java/com/nextcloud/client/EndToEndAction.java b/app/src/androidTest/java/com/nextcloud/client/EndToEndAction.java index 937cb57..08fab8c 100644 --- a/app/src/androidTest/java/com/nextcloud/client/EndToEndAction.java +++ b/app/src/androidTest/java/com/nextcloud/client/EndToEndAction.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client; diff --git a/app/src/androidTest/java/com/nextcloud/client/FileDisplayActivityIT.kt b/app/src/androidTest/java/com/nextcloud/client/FileDisplayActivityIT.kt index 94a8f9f..ad6c28f 100644 --- a/app/src/androidTest/java/com/nextcloud/client/FileDisplayActivityIT.kt +++ b/app/src/androidTest/java/com/nextcloud/client/FileDisplayActivityIT.kt @@ -3,26 +3,26 @@ * * SPDX-FileCopyrightText: 2019 Tobias Kaminsky * SPDX-FileCopyrightText: 2019 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ + package com.nextcloud.client -import android.app.Activity +import androidx.test.core.app.launchActivity import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.closeSoftKeyboard import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.DrawerActions import androidx.test.espresso.contrib.NavigationViewActions import androidx.test.espresso.contrib.RecyclerViewActions -import androidx.test.espresso.intent.rule.IntentsTestRule import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText -import androidx.test.platform.app.InstrumentationRegistry import com.nextcloud.test.RetryTestRule import com.owncloud.android.AbstractOnServerIT import com.owncloud.android.R @@ -35,33 +35,39 @@ import com.owncloud.android.lib.resources.shares.ShareType import com.owncloud.android.operations.CreateFolderOperation import com.owncloud.android.ui.activity.FileDisplayActivity import com.owncloud.android.ui.adapter.OCFileListItemViewHolder -import org.junit.Assert +import com.owncloud.android.utils.EspressoIdlingResource +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Rule import org.junit.Test class FileDisplayActivityIT : AbstractOnServerIT() { - @get:Rule - val activityRule = IntentsTestRule( - FileDisplayActivity::class.java, - true, - false - ) + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } @get:Rule val retryRule = RetryTestRule() // showShares is flaky - // @ScreenshotTest // todo run without real server + @Suppress("DEPRECATION") @Test fun showShares() { - Assert.assertTrue(ExistenceCheckRemoteOperation("/shareToAdmin/", true).execute(client).isSuccess) - Assert.assertTrue(CreateFolderRemoteOperation("/shareToAdmin/", true).execute(client).isSuccess) - Assert.assertTrue(CreateFolderRemoteOperation("/shareToGroup/", true).execute(client).isSuccess) - Assert.assertTrue(CreateFolderRemoteOperation("/shareViaLink/", true).execute(client).isSuccess) - Assert.assertTrue(CreateFolderRemoteOperation("/noShare/", true).execute(client).isSuccess) - // assertTrue(new CreateFolderRemoteOperation("/shareToCircle/", true).execute(client).isSuccess()); + assertTrue(ExistenceCheckRemoteOperation("/shareToAdmin/", true).execute(client).isSuccess) + assertTrue(CreateFolderRemoteOperation("/shareToAdmin/", true).execute(client).isSuccess) + assertTrue(CreateFolderRemoteOperation("/shareToGroup/", true).execute(client).isSuccess) + assertTrue(CreateFolderRemoteOperation("/shareViaLink/", true).execute(client).isSuccess) + assertTrue(CreateFolderRemoteOperation("/noShare/", true).execute(client).isSuccess) // share folder to user "admin" - Assert.assertTrue( + assertTrue( CreateShareRemoteOperation( "/shareToAdmin/", ShareType.USER, @@ -73,7 +79,7 @@ class FileDisplayActivityIT : AbstractOnServerIT() { ) // share folder via public link - Assert.assertTrue( + assertTrue( CreateShareRemoteOperation( "/shareViaLink/", ShareType.PUBLIC_LINK, @@ -85,7 +91,7 @@ class FileDisplayActivityIT : AbstractOnServerIT() { ) // share folder to group - Assert.assertTrue( + assertTrue( CreateShareRemoteOperation( "/shareToGroup/", ShareType.GROUP, @@ -96,101 +102,53 @@ class FileDisplayActivityIT : AbstractOnServerIT() { ).execute(client).isSuccess ) - // share folder to circle - // get share -// RemoteOperationResult searchResult = new GetShareesRemoteOperation("publicCircle", 1, 50).execute(client); -// assertTrue(searchResult.getLogMessage(), searchResult.isSuccess()); -// -// JSONObject resultJson = (JSONObject) searchResult.getData().get(0); -// String circleId = resultJson.getJSONObject("value").getString("shareWith"); -// -// assertTrue(new CreateShareRemoteOperation("/shareToCircle/", -// ShareType.CIRCLE, -// circleId, -// false, -// "", -// OCShare.DEFAULT_PERMISSION) -// .execute(client).isSuccess()); + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + // open drawer + onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()) - val sut: Activity = activityRule.launchActivity(null) - InstrumentationRegistry.getInstrumentation().waitForIdleSync() - - // open drawer - onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()) - - // click "shared" - onView(withId(R.id.nav_view)) - .perform(NavigationViewActions.navigateTo(R.id.nav_shared)) - shortSleep() - shortSleep() - // screenshot(sut) // todo run without real server + // click "shared" + onView(withId(R.id.nav_view)) + .perform(NavigationViewActions.navigateTo(R.id.nav_shared)) + } + } + } } + @Suppress("DEPRECATION") @Test fun allFiles() { - val sut = activityRule.launchActivity(null) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + // given test folder + assertTrue( + CreateFolderOperation("/test/", user, targetContext, storageManager) + .execute(client) + .isSuccess + ) - // given test folder - Assert.assertTrue( - CreateFolderOperation("/test/", user, targetContext, storageManager) - .execute(client) - .isSuccess - ) + // navigate into it + val test = storageManager.getFileByPath("/test/") + sut.file = test + sut.startSyncFolderOperation(test, false) + assertEquals(storageManager.getFileByPath("/test/"), sut.currentDir) + EspressoIdlingResource.decrement() - // navigate into it - val test = storageManager.getFileByPath("/test/") - sut.file = test - sut.startSyncFolderOperation(test, false) - Assert.assertEquals(storageManager.getFileByPath("/test/"), sut.currentDir) + // open drawer + onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()) - // open drawer - onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()) + // click "all files" + onView(withId(R.id.nav_view)) + .perform(NavigationViewActions.navigateTo(R.id.nav_all_files)) - // click "all files" - onView(withId(R.id.nav_view)) - .perform(NavigationViewActions.navigateTo(R.id.nav_all_files)) - - // then should be in root again - shortSleep() - Assert.assertEquals(storageManager.getFileByPath("/"), sut.currentDir) - } - - @Test - fun checkToolbarTitleOnNavigation() { - // Create folder structure - val topFolder = "folder1" - val childFolder = "folder2" - - CreateFolderOperation("/$topFolder/", user, targetContext, storageManager) - .execute(client) - - CreateFolderOperation("/$topFolder/$childFolder/", user, targetContext, storageManager) - .execute(client) - - activityRule.launchActivity(null) - - shortSleep() - - // go into "foo" - onView(withText(topFolder)).perform(click()) - shortSleep() - - // check title is right - checkToolbarTitle(topFolder) - - // go into "bar" - onView(withText(childFolder)).perform(click()) - shortSleep() - - // check title is right - checkToolbarTitle(childFolder) - - // browse back up, we should be back in "foo" - Espresso.pressBack() - shortSleep() - - // check title is right - checkToolbarTitle(topFolder) + // then should be in root again + assertEquals(storageManager.getFileByPath("/"), sut.currentDir) + } + } + } } private fun checkToolbarTitle(childFolder: String) { @@ -203,8 +161,10 @@ class FileDisplayActivityIT : AbstractOnServerIT() { ) } + @Suppress("DEPRECATION") @Test fun browseFavoriteAndBack() { + EspressoIdlingResource.increment() // Create folder structure val topFolder = "folder1" @@ -212,52 +172,68 @@ class FileDisplayActivityIT : AbstractOnServerIT() { .execute(client) ToggleFavoriteRemoteOperation(true, "/$topFolder/") .execute(client) + EspressoIdlingResource.decrement() - val sut = activityRule.launchActivity(null) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + // navigate to favorites + onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()) + onView(withId(R.id.nav_view)) + .perform(NavigationViewActions.navigateTo(R.id.nav_favorites)) - // navigate to favorites - onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()) - onView(withId(R.id.nav_view)) - .perform(NavigationViewActions.navigateTo(R.id.nav_favorites)) - shortSleep() + // check sort button is not shown, favorites are not sortable + onView( + withId(R.id.sort_button) + ).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))) - // check sort button is not shown, favorites are not sortable - onView(withId(R.id.sort_button)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))) + // browse into folder + onView(withId(R.id.list_root)) + .perform(closeSoftKeyboard()) + .perform( + RecyclerViewActions.actionOnItemAtPosition( + 0, + click() + ) + ) + checkToolbarTitle(topFolder) + // sort button should now be visible + onView(withId(R.id.sort_button)).check(matches(ViewMatchers.isDisplayed())) - // browse into folder - onView(withId(R.id.list_root)) - .perform(closeSoftKeyboard()) - .perform( - RecyclerViewActions.actionOnItemAtPosition( - 0, - click() - ) - ) - shortSleep() - checkToolbarTitle(topFolder) - // sort button should now be visible - onView(withId(R.id.sort_button)).check(matches(ViewMatchers.isDisplayed())) - - // browse back, should be back to All Files - Espresso.pressBack() - checkToolbarTitle(sut.getString(R.string.app_name)) - onView(withId(R.id.sort_button)).check(matches(ViewMatchers.isDisplayed())) + // browse back, should be back to All Files + Espresso.pressBack() + checkToolbarTitle(sut.getString(R.string.app_name)) + onView(withId(R.id.sort_button)).check(matches(ViewMatchers.isDisplayed())) + } + } + } } + @Suppress("DEPRECATION") @Test fun switchToGridView() { - activityRule.launchActivity(null) - Assert.assertTrue( - CreateFolderOperation("/test/", user, targetContext, storageManager) - .execute(client) - .isSuccess - ) - onView(withId(R.id.switch_grid_view_button)).perform(click()) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + assertTrue( + CreateFolderOperation("/test/", user, targetContext, storageManager) + .execute(client) + .isSuccess + ) + onView(withId(R.id.switch_grid_view_button)).perform(click()) + } + } + } } @Test fun openAccountSwitcher() { - activityRule.launchActivity(null) - onView(withId(R.id.switch_account_button)).perform(click()) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + onView(withId(R.id.switch_account_button)).perform(click()) + } + } + } } } diff --git a/app/src/androidTest/java/com/nextcloud/client/FileDisplayActivityScreenshotIT.kt b/app/src/androidTest/java/com/nextcloud/client/FileDisplayActivityScreenshotIT.kt index caa0e47..2b7c1e8 100644 --- a/app/src/androidTest/java/com/nextcloud/client/FileDisplayActivityScreenshotIT.kt +++ b/app/src/androidTest/java/com/nextcloud/client/FileDisplayActivityScreenshotIT.kt @@ -1,35 +1,50 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2019 Tobias Kaminsky * SPDX-FileCopyrightText: 2019 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client import android.Manifest -import androidx.test.espresso.Espresso +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.DrawerActions import androidx.test.espresso.contrib.NavigationViewActions -import androidx.test.espresso.intent.rule.IntentsTestRule import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.rule.GrantPermissionRule import com.owncloud.android.AbstractIT import com.owncloud.android.R import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.fragment.EmptyListState +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.ScreenshotTest +import org.junit.After import org.junit.Assert +import org.junit.Before import org.junit.Rule import org.junit.Test class FileDisplayActivityScreenshotIT : AbstractIT() { - @get:Rule - val activityRule = IntentsTestRule( - FileDisplayActivity::class.java, - true, - false - ) + private val testClassName = "com.nextcloud.client.FileDisplayActivityScreenshotIT" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } @get:Rule val permissionRule: GrantPermissionRule = GrantPermissionRule.grant( @@ -41,82 +56,110 @@ class FileDisplayActivityScreenshotIT : AbstractIT() { } @Test + @UiThread @ScreenshotTest fun open() { try { - val sut = activityRule.launchActivity(null) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() - shortSleep() - sut.runOnUiThread { - sut.listOfFilesFragment!!.setFabEnabled(false) - sut.resetScrolling(true) - sut.listOfFilesFragment!!.setEmptyListLoadingMessage() - sut.listOfFilesFragment!!.isLoading = false + sut.run { + listOfFilesFragment?.let { + it.setFabEnabled(false) + resetScrolling(true) + it.setEmptyListMessage(EmptyListState.LOADING) + it.isLoading = false + } + } + + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "open", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } } - shortSleep() - waitForIdleSync() - screenshot(sut) } catch (e: SecurityException) { Log_OC.e(TAG, "Error caught at open $e") } } @Test + @UiThread @ScreenshotTest fun showMediaThenAllFiles() { try { - val fileDisplayActivity = activityRule.launchActivity(null) - val sut = fileDisplayActivity.listOfFilesFragment - Assert.assertNotNull(sut) - sut!!.setFabEnabled(false) - sut.setEmptyListLoadingMessage() - sut.isLoading = false + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + val fragment = sut.listOfFilesFragment + Assert.assertNotNull(fragment) + fragment!!.setFabEnabled(false) + fragment.setEmptyListMessage(EmptyListState.LOADING) + fragment.isLoading = false + EspressoIdlingResource.decrement() - // open drawer - Espresso.onView(ViewMatchers.withId(R.id.drawer_layout)).perform(DrawerActions.open()) + onView(ViewMatchers.withId(R.id.drawer_layout)).perform(DrawerActions.open()) - // click "all files" - Espresso.onView(ViewMatchers.withId(R.id.nav_view)) - .perform(NavigationViewActions.navigateTo(R.id.nav_gallery)) + onView(ViewMatchers.withId(R.id.nav_view)) + .perform(NavigationViewActions.navigateTo(R.id.nav_gallery)) - // wait - shortSleep() + onView(ViewMatchers.withId(R.id.drawer_layout)).perform(DrawerActions.open()) + onView(ViewMatchers.withId(R.id.nav_view)) + .perform(NavigationViewActions.navigateTo(R.id.nav_all_files)) - // click "all files" - Espresso.onView(ViewMatchers.withId(R.id.drawer_layout)).perform(DrawerActions.open()) - Espresso.onView(ViewMatchers.withId(R.id.nav_view)) - .perform(NavigationViewActions.navigateTo(R.id.nav_all_files)) + EspressoIdlingResource.increment() + fragment.setFabEnabled(false) + fragment.setEmptyListMessage(EmptyListState.LOADING) + fragment.isLoading = false + EspressoIdlingResource.decrement() - // then compare screenshot - shortSleep() - sut.setFabEnabled(false) - sut.setEmptyListLoadingMessage() - sut.isLoading = false - shortSleep() - screenshot(fileDisplayActivity) + val screenShotName = createName(testClassName + "_" + "showMediaThenAllFiles", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } catch (e: SecurityException) { Log_OC.e(TAG, "Error caught at open $e") } } @Test + @UiThread @ScreenshotTest fun drawer() { try { - val sut = activityRule.launchActivity(null) - Espresso.onView(ViewMatchers.withId(R.id.drawer_layout)).perform(DrawerActions.open()) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + onView(ViewMatchers.withId(R.id.drawer_layout)).perform(DrawerActions.open()) - shortSleep() - sut.runOnUiThread { - sut.hideInfoBox() - sut.resetScrolling(true) - sut.listOfFilesFragment!!.setFabEnabled(false) - sut.listOfFilesFragment!!.setEmptyListLoadingMessage() - sut.listOfFilesFragment!!.isLoading = false + EspressoIdlingResource.increment() + + sut.run { + hideInfoBox() + resetScrolling(true) + + listOfFilesFragment?.let { + it.setFabEnabled(false) + it.setEmptyListMessage(EmptyListState.LOADING) + it.isLoading = false + } + } + + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "drawer", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } } - shortSleep() - waitForIdleSync() - screenshot(sut) } catch (e: SecurityException) { Log_OC.e(TAG, "Error caught at open $e") } diff --git a/app/src/androidTest/java/com/nextcloud/client/FirstRunActivityIT.java b/app/src/androidTest/java/com/nextcloud/client/FirstRunActivityIT.java deleted file mode 100644 index b025a25..0000000 --- a/app/src/androidTest/java/com/nextcloud/client/FirstRunActivityIT.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2019 Tobias Kaminsky - * SPDX-FileCopyrightText: 2019 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.nextcloud.client; - -import android.app.Activity; - -import com.nextcloud.client.onboarding.FirstRunActivity; -import com.owncloud.android.AbstractIT; -import com.owncloud.android.utils.ScreenshotTest; - -import org.junit.Rule; -import org.junit.Test; - -import androidx.test.espresso.intent.rule.IntentsTestRule; - -public class FirstRunActivityIT extends AbstractIT { - @Rule public IntentsTestRule activityRule = new IntentsTestRule<>(FirstRunActivity.class, - true, - false); - - @Test - @ScreenshotTest - public void open() { - Activity sut = activityRule.launchActivity(null); - - screenshot(sut); - } - -} diff --git a/app/src/androidTest/java/com/nextcloud/client/FirstRunActivityIT.kt b/app/src/androidTest/java/com/nextcloud/client/FirstRunActivityIT.kt new file mode 100644 index 0000000..23df993 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/client/FirstRunActivityIT.kt @@ -0,0 +1,53 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-FileCopyrightText: 2019 Tobias Kaminsky + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.nextcloud.client + +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import com.nextcloud.client.onboarding.FirstRunActivity +import com.owncloud.android.AbstractIT +import com.owncloud.android.utils.EspressoIdlingResource +import com.owncloud.android.utils.ScreenshotTest +import org.junit.After +import org.junit.Before +import org.junit.Test + +class FirstRunActivityIT : AbstractIT() { + private val testClassName = "com.nextcloud.client.FirstRunActivityIT" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } + + @Test + @UiThread + @ScreenshotTest + fun open() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + val screenShotName = createName(testClassName + "_" + "open", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } + } +} diff --git a/app/src/androidTest/java/com/nextcloud/client/SettingsActivityIT.kt b/app/src/androidTest/java/com/nextcloud/client/SettingsActivityIT.kt index dc8878f..193c210 100644 --- a/app/src/androidTest/java/com/nextcloud/client/SettingsActivityIT.kt +++ b/app/src/androidTest/java/com/nextcloud/client/SettingsActivityIT.kt @@ -1,71 +1,109 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2019 Tobias Kaminsky * SPDX-FileCopyrightText: 2019 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client -import android.app.Activity import android.content.Intent import android.os.Looper -import androidx.test.espresso.intent.rule.IntentsTestRule -import com.nextcloud.test.GrantStoragePermissionRule +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import com.nextcloud.test.GrantStoragePermissionRule.Companion.grant import com.owncloud.android.AbstractIT import com.owncloud.android.datamodel.ArbitraryDataProviderImpl import com.owncloud.android.ui.activity.RequestCredentialsActivity import com.owncloud.android.ui.activity.SettingsActivity import com.owncloud.android.utils.EncryptionUtils +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.ScreenshotTest +import org.junit.After import org.junit.Assert +import org.junit.Before import org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule @Suppress("FunctionNaming") class SettingsActivityIT : AbstractIT() { - @get:Rule - val activityRule = IntentsTestRule( - SettingsActivity::class.java, - true, - false - ) + private val testClassName = "com.nextcloud.client.SettingsActivityIT" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } @get:Rule - val permissionRule = GrantStoragePermissionRule.grant() + var storagePermissionRule: TestRule = grant() @Test + @UiThread @ScreenshotTest fun open() { - val sut: Activity = activityRule.launchActivity(null) - screenshot(sut) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + val screenShotName = createName(testClassName + "_" + "open", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } @Test + @UiThread @ScreenshotTest fun showMnemonic_Error() { - val sut = activityRule.launchActivity(null) - sut.handleMnemonicRequest(null) - shortSleep() - waitForIdleSync() - screenshot(sut) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + sut.handleMnemonicRequest(null) + val screenShotName = createName(testClassName + "_" + "showMnemonic_Error", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } + @Suppress("DEPRECATION") @Test + @UiThread fun showMnemonic() { if (Looper.myLooper() == null) { Looper.prepare() } - val intent = Intent() - intent.putExtra(RequestCredentialsActivity.KEY_CHECK_RESULT, RequestCredentialsActivity.KEY_CHECK_RESULT_TRUE) - val arbitraryDataProvider = ArbitraryDataProviderImpl(targetContext) - arbitraryDataProvider.storeOrUpdateKeyValue(user.accountName, EncryptionUtils.MNEMONIC, "Secret mnemonic") - val sut = activityRule.launchActivity(null) - sut.runOnUiThread { - sut.handleMnemonicRequest(intent) + val intent = Intent().apply { + putExtra(RequestCredentialsActivity.KEY_CHECK_RESULT, RequestCredentialsActivity.KEY_CHECK_RESULT_TRUE) } - Looper.myLooper()?.quitSafely() - Assert.assertTrue(true) // if we reach this, everything is ok + ArbitraryDataProviderImpl(targetContext).run { + storeOrUpdateKeyValue(user.accountName, EncryptionUtils.MNEMONIC, "Secret mnemonic") + } + + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + sut.handleMnemonicRequest(intent) + onView(isRoot()).check(matches(isDisplayed())) + Looper.myLooper()?.quitSafely() + Assert.assertTrue(true) + } + } + } } } diff --git a/app/src/androidTest/java/com/nextcloud/client/SyncedFoldersActivityIT.java b/app/src/androidTest/java/com/nextcloud/client/SyncedFoldersActivityIT.java deleted file mode 100644 index ba43a43..0000000 --- a/app/src/androidTest/java/com/nextcloud/client/SyncedFoldersActivityIT.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.nextcloud.client; - -import android.app.Activity; -import android.content.Intent; - -import com.nextcloud.client.preferences.SubFolderRule; -import com.owncloud.android.AbstractIT; -import com.owncloud.android.databinding.SyncedFoldersLayoutBinding; -import com.owncloud.android.datamodel.MediaFolderType; -import com.owncloud.android.datamodel.SyncedFolder; -import com.owncloud.android.datamodel.SyncedFolderDisplayItem; -import com.owncloud.android.ui.activity.SyncedFoldersActivity; -import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment; -import com.owncloud.android.utils.ScreenshotTest; - -import org.junit.Rule; -import org.junit.Test; - -import java.util.Objects; - -import androidx.test.espresso.intent.rule.IntentsTestRule; - -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; - - -public class SyncedFoldersActivityIT extends AbstractIT { - @Rule public IntentsTestRule activityRule = new IntentsTestRule<>(SyncedFoldersActivity.class, - true, - false); - - @Test - @ScreenshotTest - public void open() { - SyncedFoldersActivity activity = activityRule.launchActivity(null); - activity.adapter.clear(); - SyncedFoldersLayoutBinding sut = activity.binding; - shortSleep(); - screenshot(sut.emptyList.emptyListView); - } - - @Test - @ScreenshotTest - public void testSyncedFolderDialog() { - SyncedFolderDisplayItem item = new SyncedFolderDisplayItem(1, - "/sdcard/DCIM/", - "/InstantUpload/", - true, - false, - false, - true, - "test@https://nextcloud.localhost", - 0, - 0, - true, - 1000, - "Name", - MediaFolderType.IMAGE, - false, - SubFolderRule.YEAR_MONTH, - false, - SyncedFolder.NOT_SCANNED_YET); - SyncedFolderPreferencesDialogFragment sut = SyncedFolderPreferencesDialogFragment.newInstance(item, 0); - - Intent intent = new Intent(targetContext, SyncedFoldersActivity.class); - SyncedFoldersActivity activity = activityRule.launchActivity(intent); - - sut.show(activity.getSupportFragmentManager(), ""); - - getInstrumentation().waitForIdleSync(); - shortSleep(); - - screenshot(Objects.requireNonNull(sut.requireDialog().getWindow()).getDecorView()); - } -} diff --git a/app/src/androidTest/java/com/nextcloud/client/SyncedFoldersActivityIT.kt b/app/src/androidTest/java/com/nextcloud/client/SyncedFoldersActivityIT.kt new file mode 100644 index 0000000..0709359 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/client/SyncedFoldersActivityIT.kt @@ -0,0 +1,133 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.nextcloud.client + +import android.content.Intent +import android.os.Looper +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import com.nextcloud.client.preferences.SubFolderRule +import com.owncloud.android.AbstractIT +import com.owncloud.android.datamodel.MediaFolderType +import com.owncloud.android.datamodel.SyncedFolder +import com.owncloud.android.datamodel.SyncedFolderDisplayItem +import com.owncloud.android.ui.activity.SyncedFoldersActivity +import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment.Companion.newInstance +import com.owncloud.android.utils.EspressoIdlingResource +import com.owncloud.android.utils.ScreenshotTest +import org.junit.After +import org.junit.Before +import org.junit.Test + +class SyncedFoldersActivityIT : AbstractIT() { + private val testClassName = "com.nextcloud.client.SyncedFoldersActivityIT" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } + + @Test + @UiThread + @ScreenshotTest + fun open() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + sut.adapter.clear() + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "open", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut.binding.emptyList.emptyListView, screenShotName) + } + } + } + } + + @Test + @UiThread + @ScreenshotTest + fun testSyncedFolderDialog() { + val item = SyncedFolderDisplayItem( + 1, + "/sdcard/DCIM/", + "/InstantUpload/", + true, + false, + false, + true, + "test@https://nextcloud.localhost", + 0, + 0, + true, + 1000, + "Name", + MediaFolderType.IMAGE, + false, + SubFolderRule.YEAR_MONTH, + false, + SyncedFolder.NOT_SCANNED_YET + ) + val fragment = newInstance(item, 0) + + val intent = Intent(targetContext, SyncedFoldersActivity::class.java) + launchActivity(intent).use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + fragment?.show(sut.supportFragmentManager, "") + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "testSyncedFolderDialog", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshot(fragment?.requireDialog()?.window?.decorView, screenShotName) + } + } + } + } + + @Test + @UiThread + @ScreenshotTest + fun showPowerCheckDialog() { + if (Looper.myLooper() == null) { + Looper.prepare() + } + + val intent = Intent(targetContext, SyncedFoldersActivity::class.java) + + launchActivity(intent).use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + val dialog = sut.buildPowerCheckDialog() + sut.showPowerCheckDialog() + + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "showPowerCheckDialog", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshot(dialog.window?.decorView, screenShotName) + } + } + } + } +} diff --git a/app/src/androidTest/java/com/nextcloud/client/TestRunner.kt b/app/src/androidTest/java/com/nextcloud/client/TestRunner.kt index 37beeb5..d17ebcd 100644 --- a/app/src/androidTest/java/com/nextcloud/client/TestRunner.kt +++ b/app/src/androidTest/java/com/nextcloud/client/TestRunner.kt @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2023 Álvaro Brey * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH * SPDX-FileCopyrightText: 2019 Tobias Kaminsky - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client diff --git a/app/src/androidTest/java/com/nextcloud/client/UploadListActivityActivityIT.java b/app/src/androidTest/java/com/nextcloud/client/UploadListActivityActivityIT.java deleted file mode 100644 index c8415b0..0000000 --- a/app/src/androidTest/java/com/nextcloud/client/UploadListActivityActivityIT.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.nextcloud.client; - -import com.owncloud.android.AbstractIT; -import com.owncloud.android.ui.activity.UploadListActivity; -import com.owncloud.android.utils.ScreenshotTest; - -import org.junit.Rule; -import org.junit.Test; - -import androidx.test.espresso.intent.rule.IntentsTestRule; - - -public class UploadListActivityActivityIT extends AbstractIT { - @Rule public IntentsTestRule activityRule = new IntentsTestRule<>(UploadListActivity.class, - true, - false); - - @Test - @ScreenshotTest - public void openDrawer() { - super.openDrawer(activityRule); - } -} diff --git a/app/src/androidTest/java/com/nextcloud/client/UploadListActivityActivityIT.kt b/app/src/androidTest/java/com/nextcloud/client/UploadListActivityActivityIT.kt new file mode 100644 index 0000000..a01cdd5 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/client/UploadListActivityActivityIT.kt @@ -0,0 +1,58 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.nextcloud.client + +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.contrib.DrawerActions +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import androidx.test.espresso.matcher.ViewMatchers.withId +import com.owncloud.android.AbstractIT +import com.owncloud.android.R +import com.owncloud.android.ui.activity.UploadListActivity +import com.owncloud.android.utils.EspressoIdlingResource +import com.owncloud.android.utils.ScreenshotTest +import org.junit.After +import org.junit.Before +import org.junit.Test + +class UploadListActivityActivityIT : AbstractIT() { + private val testClassName = "com.nextcloud.client.UploadListActivityActivityIT" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } + + @Test + @UiThread + @ScreenshotTest + fun openDrawer() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + onView(isRoot()).check(matches(isDisplayed())) + onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()) + + val screenShotName = createName(testClassName + "_" + "openDrawer", "") + screenshotViaName(sut, screenShotName) + } + } + } + } +} diff --git a/app/src/androidTest/java/com/nextcloud/client/account/AnonymousUserTest.kt b/app/src/androidTest/java/com/nextcloud/client/account/AnonymousUserTest.kt index 23da22a..7def852 100644 --- a/app/src/androidTest/java/com/nextcloud/client/account/AnonymousUserTest.kt +++ b/app/src/androidTest/java/com/nextcloud/client/account/AnonymousUserTest.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.account diff --git a/app/src/androidTest/java/com/nextcloud/client/account/MockUserTest.kt b/app/src/androidTest/java/com/nextcloud/client/account/MockUserTest.kt index e3b6f67..f45afff 100644 --- a/app/src/androidTest/java/com/nextcloud/client/account/MockUserTest.kt +++ b/app/src/androidTest/java/com/nextcloud/client/account/MockUserTest.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.account diff --git a/app/src/androidTest/java/com/nextcloud/client/account/OwnCloudClientManagerTest.java b/app/src/androidTest/java/com/nextcloud/client/account/OwnCloudClientManagerTest.java index 61f2dc3..1c832c1 100644 --- a/app/src/androidTest/java/com/nextcloud/client/account/OwnCloudClientManagerTest.java +++ b/app/src/androidTest/java/com/nextcloud/client/account/OwnCloudClientManagerTest.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2019 Tobias Kaminsky * SPDX-FileCopyrightText: 2019 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.account; diff --git a/app/src/androidTest/java/com/nextcloud/client/account/RegisteredUserTest.kt b/app/src/androidTest/java/com/nextcloud/client/account/RegisteredUserTest.kt index 52b517a..d429c4a 100644 --- a/app/src/androidTest/java/com/nextcloud/client/account/RegisteredUserTest.kt +++ b/app/src/androidTest/java/com/nextcloud/client/account/RegisteredUserTest.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.account @@ -12,7 +12,7 @@ import android.net.Uri import android.os.Parcel import com.owncloud.android.lib.common.OwnCloudAccount import com.owncloud.android.lib.common.OwnCloudBasicCredentials -import com.owncloud.android.lib.resources.status.OwnCloudVersion +import com.owncloud.android.lib.resources.status.NextcloudVersion import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotSame @@ -31,7 +31,7 @@ class RegisteredUserTest { val ownCloudAccount = OwnCloudAccount(uri, credentials) val server = Server( uri = URI(uri.toString()), - version = OwnCloudVersion.nextcloud_17 + version = NextcloudVersion.nextcloud_31 ) return RegisteredUser( account = account, diff --git a/app/src/androidTest/java/com/nextcloud/client/account/UserAccountManagerImplTest.java b/app/src/androidTest/java/com/nextcloud/client/account/UserAccountManagerImplTest.java index d24aae4..053759d 100644 --- a/app/src/androidTest/java/com/nextcloud/client/account/UserAccountManagerImplTest.java +++ b/app/src/androidTest/java/com/nextcloud/client/account/UserAccountManagerImplTest.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019-2023 Tobias Kaminsky - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.account; diff --git a/app/src/androidTest/java/com/nextcloud/client/assistant/AssistantRepositoryTests.kt b/app/src/androidTest/java/com/nextcloud/client/assistant/AssistantRepositoryTests.kt index 20cf85b..5463af8 100644 --- a/app/src/androidTest/java/com/nextcloud/client/assistant/AssistantRepositoryTests.kt +++ b/app/src/androidTest/java/com/nextcloud/client/assistant/AssistantRepositoryTests.kt @@ -1,14 +1,15 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2024 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.assistant -import com.nextcloud.client.assistant.repository.AssistantRepository +import com.nextcloud.client.assistant.repository.remote.AssistantRemoteRepositoryImpl import com.owncloud.android.AbstractOnServerIT +import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData import com.owncloud.android.lib.resources.status.NextcloudVersion import org.junit.Assert.assertTrue import org.junit.Before @@ -17,11 +18,11 @@ import org.junit.Test @Suppress("MagicNumber") class AssistantRepositoryTests : AbstractOnServerIT() { - private var sut: AssistantRepository? = null + private var sut: AssistantRemoteRepositoryImpl? = null @Before fun setup() { - sut = AssistantRepository(nextcloudClient) + sut = AssistantRemoteRepositoryImpl(nextcloudClient, capability) } @Test @@ -33,10 +34,7 @@ class AssistantRepositoryTests : AbstractOnServerIT() { } val result = sut?.getTaskTypes() - assertTrue(result?.isSuccess == true) - - val taskTypes = result?.resultData?.types - assertTrue(taskTypes?.isNotEmpty() == true) + assertTrue(result?.isNotEmpty() == true) } @Test @@ -48,10 +46,7 @@ class AssistantRepositoryTests : AbstractOnServerIT() { } val result = sut?.getTaskList("assistant") - assertTrue(result?.isSuccess == true) - - val taskList = result?.resultData?.tasks - assertTrue(taskList?.isEmpty() == true || (taskList?.size ?: 0) > 0) + assertTrue(result?.isEmpty() == true || (result?.size ?: 0) > 0) } @Test @@ -63,8 +58,14 @@ class AssistantRepositoryTests : AbstractOnServerIT() { } val input = "Give me some random output for test purpose" - val type = "OCP\\TextProcessing\\FreePromptTaskType" - val result = sut?.createTask(input, type) + val taskType = TaskTypeData( + "core:text2text", + "Free text to text prompt", + "Runs an arbitrary prompt through a language model that returns a reply", + emptyMap(), + emptyMap() + ) + val result = sut?.createTask(input, taskType) assertTrue(result?.isSuccess == true) } @@ -80,13 +81,11 @@ class AssistantRepositoryTests : AbstractOnServerIT() { sleep(120) - val resultOfTaskList = sut?.getTaskList("assistant") - assertTrue(resultOfTaskList?.isSuccess == true) + val taskList = sut?.getTaskList("assistant") + assertTrue(taskList != null) sleep(120) - val taskList = resultOfTaskList?.resultData?.tasks - assert((taskList?.size ?: 0) > 0) val result = sut?.deleteTask(taskList!!.first().id) diff --git a/app/src/androidTest/java/com/nextcloud/client/database/migrations/MigrationTest.kt b/app/src/androidTest/java/com/nextcloud/client/database/migrations/MigrationTest.kt index af0d3ab..00e96dc 100644 --- a/app/src/androidTest/java/com/nextcloud/client/database/migrations/MigrationTest.kt +++ b/app/src/androidTest/java/com/nextcloud/client/database/migrations/MigrationTest.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Álvaro Brey * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.database.migrations diff --git a/app/src/androidTest/java/com/nextcloud/client/documentscan/GeneratePDFUseCaseTest.kt b/app/src/androidTest/java/com/nextcloud/client/documentscan/GeneratePDFUseCaseTest.kt index d5f1c8f..36402fb 100644 --- a/app/src/androidTest/java/com/nextcloud/client/documentscan/GeneratePDFUseCaseTest.kt +++ b/app/src/androidTest/java/com/nextcloud/client/documentscan/GeneratePDFUseCaseTest.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Álvaro Brey * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.documentscan diff --git a/app/src/androidTest/java/com/nextcloud/client/etm/EtmActivityTest.kt b/app/src/androidTest/java/com/nextcloud/client/etm/EtmActivityTest.kt index 3b23cad..7297dfc 100644 --- a/app/src/androidTest/java/com/nextcloud/client/etm/EtmActivityTest.kt +++ b/app/src/androidTest/java/com/nextcloud/client/etm/EtmActivityTest.kt @@ -3,39 +3,68 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.etm -import android.app.Activity -import androidx.test.espresso.intent.rule.IntentsTestRule -import androidx.test.internal.runner.junit4.statement.UiThreadStatement +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot import com.owncloud.android.AbstractIT +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.ScreenshotTest -import org.junit.Rule +import org.junit.After +import org.junit.Before import org.junit.Test class EtmActivityTest : AbstractIT() { - @get:Rule - var activityRule = IntentsTestRule(EtmActivity::class.java, true, false) + private val testClassName = "com.nextcloud.client.etm.EtmActivityTest" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } @Test + @UiThread @ScreenshotTest fun overview() { - val sut: Activity = activityRule.launchActivity(null) - - waitForIdleSync() - - screenshot(sut) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + val screenShotName = createName(testClassName + "_" + "overview", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } @Test + @UiThread @ScreenshotTest fun accounts() { - val sut: EtmActivity = activityRule.launchActivity(null) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + sut.vm.onPageSelected(1) + EspressoIdlingResource.decrement() - UiThreadStatement.runOnUiThread { sut.vm.onPageSelected(1) } - - screenshot(sut) + val screenShotName = createName(testClassName + "_" + "accounts", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } } diff --git a/app/src/androidTest/java/com/nextcloud/client/files/DeepLinkHandlerTest.kt b/app/src/androidTest/java/com/nextcloud/client/files/DeepLinkHandlerTest.kt index 1b86f91..f6a7115 100644 --- a/app/src/androidTest/java/com/nextcloud/client/files/DeepLinkHandlerTest.kt +++ b/app/src/androidTest/java/com/nextcloud/client/files/DeepLinkHandlerTest.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.files @@ -37,8 +37,8 @@ class DeepLinkHandlerTest { class DeepLinkPattern { companion object { - val FILE_ID = 1234 - val SERVER_BASE_URLS = listOf( + private const val FILE_ID = 1234 + private val SERVER_BASE_URLS = listOf( "http://hostname.net", "https://hostname.net", "http://hostname.net/subdir1", @@ -48,7 +48,7 @@ class DeepLinkHandlerTest { "http://hostname.net/subdir1/subdir2/subdir3", "https://hostname.net/subdir1/subdir2/subdir3" ) - val INDEX_PHP_PATH = listOf( + private val INDEX_PHP_PATH = listOf( "", "/index.php" ) @@ -102,12 +102,12 @@ class DeepLinkHandlerTest { const val OTHER_SERVER_BASE_URL = "https://someotherserver.net" const val SERVER_BASE_URL = "https://server.net" const val FILE_ID = "1234567890" - val DEEP_LINK = Uri.parse("$SERVER_BASE_URL/index.php/f/$FILE_ID") + val DEEP_LINK: Uri = Uri.parse("$SERVER_BASE_URL/index.php/f/$FILE_ID") fun createMockUser(serverBaseUrl: String): User { val user = mock() val uri = URI.create(serverBaseUrl) - val server = Server(uri = uri, version = OwnCloudVersion.nextcloud_19) + val server = Server(uri = uri, version = OwnCloudVersion.nextcloud_20) whenever(user.server).thenReturn(server) return user } @@ -115,8 +115,8 @@ class DeepLinkHandlerTest { @Mock lateinit var userAccountManager: UserAccountManager - lateinit var allUsers: List - lateinit var handler: DeepLinkHandler + private lateinit var allUsers: List + private lateinit var handler: DeepLinkHandler @Before fun setUp() { diff --git a/app/src/androidTest/java/com/nextcloud/client/files/download/DownloaderServiceTest.kt b/app/src/androidTest/java/com/nextcloud/client/files/download/DownloaderServiceTest.kt index 14b8e42..d343487 100644 --- a/app/src/androidTest/java/com/nextcloud/client/files/download/DownloaderServiceTest.kt +++ b/app/src/androidTest/java/com/nextcloud/client/files/download/DownloaderServiceTest.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.files.download diff --git a/app/src/androidTest/java/com/nextcloud/client/files/download/RegistryTest.kt b/app/src/androidTest/java/com/nextcloud/client/files/download/RegistryTest.kt index bf49bf4..2bf7517 100644 --- a/app/src/androidTest/java/com/nextcloud/client/files/download/RegistryTest.kt +++ b/app/src/androidTest/java/com/nextcloud/client/files/download/RegistryTest.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.files.download diff --git a/app/src/androidTest/java/com/nextcloud/client/files/download/TransferManagerConnectionTest.kt b/app/src/androidTest/java/com/nextcloud/client/files/download/TransferManagerConnectionTest.kt index 516b332..231cf26 100644 --- a/app/src/androidTest/java/com/nextcloud/client/files/download/TransferManagerConnectionTest.kt +++ b/app/src/androidTest/java/com/nextcloud/client/files/download/TransferManagerConnectionTest.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.files.download @@ -202,7 +202,7 @@ class TransferManagerConnectionTest { connection.onServiceConnected(componentName, binder) // WHEN - // is runnign flag accessed + // is running flag accessed val isRunning = connection.isRunning // THEN diff --git a/app/src/androidTest/java/com/nextcloud/client/files/download/TransferManagerTest.kt b/app/src/androidTest/java/com/nextcloud/client/files/download/TransferManagerTest.kt index 692b268..e5983fe 100644 --- a/app/src/androidTest/java/com/nextcloud/client/files/download/TransferManagerTest.kt +++ b/app/src/androidTest/java/com/nextcloud/client/files/download/TransferManagerTest.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.files.download diff --git a/app/src/androidTest/java/com/nextcloud/client/integrations/deck/DeckApiTest.kt b/app/src/androidTest/java/com/nextcloud/client/integrations/deck/DeckApiTest.kt index 784d6de..46abfc0 100644 --- a/app/src/androidTest/java/com/nextcloud/client/integrations/deck/DeckApiTest.kt +++ b/app/src/androidTest/java/com/nextcloud/client/integrations/deck/DeckApiTest.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.integrations.deck @@ -65,9 +65,7 @@ class DeckApiTest { companion object { @Parameterized.Parameters @JvmStatic - fun initParametrs(): Array { - return DeckApiImpl.DECK_APP_PACKAGES - } + fun initParametrs(): Array = DeckApiImpl.DECK_APP_PACKAGES } @Before diff --git a/app/src/androidTest/java/com/nextcloud/client/jobs/BackgroundJobManagerTest.kt b/app/src/androidTest/java/com/nextcloud/client/jobs/BackgroundJobManagerTest.kt index e90b81c..d59256f 100644 --- a/app/src/androidTest/java/com/nextcloud/client/jobs/BackgroundJobManagerTest.kt +++ b/app/src/androidTest/java/com/nextcloud/client/jobs/BackgroundJobManagerTest.kt @@ -2,10 +2,11 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs +import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer @@ -19,13 +20,19 @@ import androidx.work.WorkInfo import androidx.work.WorkManager import com.nextcloud.client.account.User import com.nextcloud.client.core.Clock +import com.nextcloud.utils.extensions.toByteArray +import com.owncloud.android.lib.common.utils.Log_OC +import org.apache.commons.io.FileUtils import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue +import org.junit.Assert.fail import org.junit.Before +import org.junit.Rule import org.junit.Test +import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith import org.junit.runners.Suite import org.mockito.ArgumentMatcher @@ -37,6 +44,8 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import java.io.File +import java.io.IOException import java.util.Date import java.util.UUID import java.util.concurrent.CountDownLatch @@ -82,9 +91,11 @@ class BackgroundJobManagerTest { internal lateinit var workManager: WorkManager internal lateinit var clock: Clock internal lateinit var backgroundJobManager: BackgroundJobManagerImpl + internal lateinit var context: Context @Before fun setUpFixture() { + context = mock() user = mock() whenever(user.accountName).thenReturn(USER_ACCOUNT_NAME) workManager = mock() @@ -302,16 +313,36 @@ class BackgroundJobManagerTest { private lateinit var jobInfo: LiveData private lateinit var request: OneTimeWorkRequest + @get:Rule + var folder: TemporaryFolder = TemporaryFolder() + @Before fun setUp() { + var selectedContactsFile: File? = null + try { + selectedContactsFile = folder.newFile("hashset_cache.txt") + } catch (_: IOException) { + Log_OC.e("ImmediateContactsImport", "error creating temporary test file in ") + fail("hashset_cache cannot be found") + } + + if (selectedContactsFile == null) { + fail("hashset_cache cannot be found") + } + val requestCaptor: KArgumentCaptor = argumentCaptor() workInfo = MutableLiveData() whenever(workManager.getWorkInfoByIdLiveData(any())).thenReturn(workInfo) + + val selectedContacts = intArrayOf(1, 2, 3) + val contractsAsByteArray = selectedContacts.toByteArray() + FileUtils.writeByteArrayToFile(selectedContactsFile, contractsAsByteArray) + jobInfo = backgroundJobManager.startImmediateContactsImport( contactsAccountName = "name", contactsAccountType = "type", vCardFilePath = "/path/to/vcard/file", - selectedContacts = intArrayOf(1, 2, 3) + selectedContactsFilePath = selectedContactsFile!!.absolutePath ) verify(workManager).enqueueUniqueWork( any(), diff --git a/app/src/androidTest/java/com/nextcloud/client/jobs/ContactsBackupIT.kt b/app/src/androidTest/java/com/nextcloud/client/jobs/ContactsBackupIT.kt index 0a5dec7..bb68dfc 100644 --- a/app/src/androidTest/java/com/nextcloud/client/jobs/ContactsBackupIT.kt +++ b/app/src/androidTest/java/com/nextcloud/client/jobs/ContactsBackupIT.kt @@ -1,8 +1,7 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ package com.nextcloud.client.jobs @@ -11,73 +10,120 @@ import android.Manifest import androidx.test.rule.GrantPermissionRule import androidx.work.WorkManager import com.nextcloud.client.core.ClockImpl +import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.client.preferences.AppPreferencesImpl import com.nextcloud.test.RetryTestRule -import com.owncloud.android.AbstractIT +import com.nextcloud.utils.extensions.toByteArray import com.owncloud.android.AbstractOnServerIT import com.owncloud.android.R import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.DownloadFileOperation import ezvcard.Ezvcard import ezvcard.VCard -import junit.framework.Assert.assertEquals -import junit.framework.Assert.assertTrue +import org.apache.commons.io.FileUtils +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Before import org.junit.Rule import org.junit.Test +import org.junit.rules.TemporaryFolder import java.io.BufferedInputStream import java.io.File import java.io.FileInputStream +import java.io.IOException class ContactsBackupIT : AbstractOnServerIT() { - val workmanager = WorkManager.getInstance(targetContext) - val preferences = AppPreferencesImpl.fromContext(targetContext) - private val backgroundJobManager = BackgroundJobManagerImpl(workmanager, ClockImpl(), preferences) + private val workManager = WorkManager.getInstance(targetContext) + private val preferences: AppPreferences = AppPreferencesImpl.fromContext(targetContext) + private val backgroundJobManager = BackgroundJobManagerImpl(workManager, ClockImpl(), preferences) @get:Rule - val writeContactsRule = GrantPermissionRule.grant(Manifest.permission.WRITE_CONTACTS) + val writeContactsRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_CONTACTS) @get:Rule - val readContactsRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS) + val readContactsRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS) @get:Rule val retryTestRule = RetryTestRule() // flaky test + @get:Rule + var folder: TemporaryFolder = TemporaryFolder() + private val vcard: String = "vcard.vcf" + private var selectedContactsFile: File? = null + + @Before + fun setup() { + try { + selectedContactsFile = folder.newFile("hashset_cache.txt") + } catch (_: IOException) { + Log_OC.e("ContactsBackupIT", "error creating temporary test file in ") + } + } @Test fun importExport() { - val intArray = IntArray(1) - intArray[0] = 0 + val intArray = intArrayOf(0) + if (selectedContactsFile == null) { + fail("hashset_cache cannot be found") + } + + val contractsAsByteArray = intArray.toByteArray() + FileUtils.writeByteArrayToFile(selectedContactsFile, contractsAsByteArray) // import file to local contacts - backgroundJobManager.startImmediateContactsImport(null, null, getFile(vcard).absolutePath, intArray) - - shortSleep() + backgroundJobManager.startImmediateContactsImport( + null, + null, + getFile(vcard).absolutePath, + selectedContactsFile!!.absolutePath + ) + longSleep() // export contact backgroundJobManager.startImmediateContactsBackup(user) - longSleep() - val backupFolder: String = targetContext.resources.getString(R.string.contacts_backup_folder) + + val folderPath: String = targetContext.resources.getString(R.string.contacts_backup_folder) + OCFile.PATH_SEPARATOR refreshFolder("/") longSleep() - - refreshFolder(backupFolder) longSleep() - val backupOCFile = storageManager.getFolderContent( - storageManager.getFileByDecryptedRemotePath(backupFolder), - false - )[0] + refreshFolder(folderPath) + longSleep() + longSleep() - assertTrue(DownloadFileOperation(user, backupOCFile, AbstractIT.targetContext).execute(client).isSuccess) + if (folderPath.isEmpty()) { + fail("folderPath cannot be empty") + } + + val folder = fileDataStorageManager.getFileByDecryptedRemotePath(folderPath) + if (folder == null) { + fail("folder cannot be null") + } + + val ocFile = storageManager.getFolderContent(folder, false).firstOrNull() + if (ocFile == null) { + fail("ocFile cannot be null") + } + + if (ocFile?.storagePath == null) { + fail("ocFile.storagePath cannot be null") + } + + assertTrue(DownloadFileOperation(user, ocFile, targetContext).execute(client).isSuccess) + + val file = ocFile?.storagePath?.let { File(it) } + if (file == null) { + fail("file cannot be null") + } - val backupFile = File(backupOCFile.storagePath) val vcardInputStream = BufferedInputStream(FileInputStream(getFile(vcard))) - val backupFileInputStream = BufferedInputStream(FileInputStream(backupFile)) + val backupFileInputStream = BufferedInputStream(FileInputStream(file)) // verify same val originalCards: ArrayList = ArrayList() @@ -87,6 +133,17 @@ class ContactsBackupIT : AbstractOnServerIT() { backupCards.addAll(Ezvcard.parse(backupFileInputStream).all()) assertEquals(originalCards.size, backupCards.size) - assertEquals(originalCards[0].formattedName.toString(), backupCards[0].formattedName.toString()) + + val originalCardFormattedName = originalCards.firstOrNull()?.formattedName + if (originalCardFormattedName == null) { + fail("originalCardFormattedName cannot be null") + } + + val backupCardFormattedName = backupCards.firstOrNull()?.formattedName + if (backupCardFormattedName == null) { + fail("backupCardFormattedName cannot be null") + } + + assertEquals(originalCardFormattedName.toString(), backupCardFormattedName.toString()) } } diff --git a/app/src/androidTest/java/com/nextcloud/client/migrations/MigrationsDbTest.kt b/app/src/androidTest/java/com/nextcloud/client/migrations/MigrationsDbTest.kt index 92602e2..035ec3d 100644 --- a/app/src/androidTest/java/com/nextcloud/client/migrations/MigrationsDbTest.kt +++ b/app/src/androidTest/java/com/nextcloud/client/migrations/MigrationsDbTest.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.migrations diff --git a/app/src/androidTest/java/com/nextcloud/client/migrations/MigrationsManagerTest.kt b/app/src/androidTest/java/com/nextcloud/client/migrations/MigrationsManagerTest.kt index af02855..0d12bda 100644 --- a/app/src/androidTest/java/com/nextcloud/client/migrations/MigrationsManagerTest.kt +++ b/app/src/androidTest/java/com/nextcloud/client/migrations/MigrationsManagerTest.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.migrations diff --git a/app/src/androidTest/java/com/nextcloud/client/migrations/MockSharedPreferences.kt b/app/src/androidTest/java/com/nextcloud/client/migrations/MockSharedPreferences.kt index 390f7ff..bd87169 100644 --- a/app/src/androidTest/java/com/nextcloud/client/migrations/MockSharedPreferences.kt +++ b/app/src/androidTest/java/com/nextcloud/client/migrations/MockSharedPreferences.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.migrations @@ -69,25 +69,16 @@ class MockSharedPreferences : SharedPreferences { override fun getInt(key: String?, defValue: Int): Int = store.getOrDefault(key, defValue) as Int - override fun getAll(): MutableMap { - return HashMap(store) - } + override fun getAll(): MutableMap = HashMap(store) - override fun edit(): SharedPreferences.Editor { - return MockEditor(store) - } + override fun edit(): SharedPreferences.Editor = MockEditor(store) - override fun getLong(key: String?, defValue: Long): Long { - throw UnsupportedOperationException() - } + override fun getLong(key: String?, defValue: Long): Long = throw UnsupportedOperationException() - override fun getFloat(key: String?, defValue: Float): Float { - throw UnsupportedOperationException() - } + override fun getFloat(key: String?, defValue: Float): Float = throw UnsupportedOperationException() - override fun getStringSet(key: String?, defValues: MutableSet?): MutableSet? { - return store.getOrDefault(key, defValues) as MutableSet? - } + override fun getStringSet(key: String?, defValues: MutableSet?): MutableSet? = + store.getOrDefault(key, defValues) as MutableSet? override fun registerOnSharedPreferenceChangeListener( listener: SharedPreferences.OnSharedPreferenceChangeListener? diff --git a/app/src/androidTest/java/com/nextcloud/client/migrations/MockSharedPreferencesTest.kt b/app/src/androidTest/java/com/nextcloud/client/migrations/MockSharedPreferencesTest.kt index 8ea5830..6654483 100644 --- a/app/src/androidTest/java/com/nextcloud/client/migrations/MockSharedPreferencesTest.kt +++ b/app/src/androidTest/java/com/nextcloud/client/migrations/MockSharedPreferencesTest.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.migrations diff --git a/app/src/androidTest/java/com/nextcloud/client/network/ConnectivityServiceImplIT.kt b/app/src/androidTest/java/com/nextcloud/client/network/ConnectivityServiceImplIT.kt index 4935106..97ce829 100644 --- a/app/src/androidTest/java/com/nextcloud/client/network/ConnectivityServiceImplIT.kt +++ b/app/src/androidTest/java/com/nextcloud/client/network/ConnectivityServiceImplIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.network diff --git a/app/src/androidTest/java/com/nextcloud/extensions/BitmapDecodeTests.kt b/app/src/androidTest/java/com/nextcloud/extensions/BitmapDecodeTests.kt new file mode 100644 index 0000000..e6b52ce --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/extensions/BitmapDecodeTests.kt @@ -0,0 +1,111 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.nextcloud.extensions + +import android.graphics.Bitmap +import com.nextcloud.utils.decodeSampledBitmapFromFile +import com.nextcloud.utils.extensions.toFile +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.io.OutputStream +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.absolutePathString +import kotlin.io.path.deleteRecursively +import kotlin.io.path.exists + +@Suppress("MagicNumber") +class BitmapDecodeTests { + + private lateinit var tempDir: Path + + @Before + fun setup() { + tempDir = Files.createTempDirectory("auto_upload_test_") + assertTrue("Temp directory should exist", tempDir.exists()) + } + + @OptIn(ExperimentalPathApi::class) + @After + fun cleanup() { + if (tempDir.exists()) { + tempDir.deleteRecursively() + } + } + + private fun createTempImageFile(width: Int = 100, height: Int = 100): Path { + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val imagePath = tempDir.resolve("test_${System.currentTimeMillis()}.jpg") + + Files.newOutputStream(imagePath).use { out: OutputStream -> + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out) + } + + assertTrue(imagePath.exists()) + return imagePath + } + + @Test + fun testToFileWhenPathIsValidShouldReturnExistingFile() { + val path = createTempImageFile() + val result = path.absolutePathString().toFile() + assertNotNull(result) + assertTrue(result!!.exists()) + } + + @Test + fun testToFileWhenPathIsEmptyShouldReturnNull() { + val result = "".toFile() + assertNull(result) + } + + @Test + fun testToFileWhenFileDoesNotExistShouldReturnNull() { + val nonExistentPath = tempDir.resolve("does_not_exist.jpg") + val result = nonExistentPath.absolutePathString().toFile() + assertNull(result) + } + + @Test + fun testDecodeSampledBitmapFromFileWhenValidPathShouldReturnBitmap() { + val path = createTempImageFile(400, 400) + val bitmap = decodeSampledBitmapFromFile(path.absolutePathString(), 100, 100) + assertNotNull(bitmap) + assertTrue(bitmap!!.width <= 400) + assertTrue(bitmap.height <= 400) + } + + @Test + fun testDecodeSampledBitmapFromFileWhenInvalidPathShouldReturnNull() { + val invalidPath = tempDir.resolve("invalid_path.jpg").absolutePathString() + val bitmap = decodeSampledBitmapFromFile(invalidPath, 100, 100) + assertNull(bitmap) + } + + @Test + fun testDecodeSampledBitmapFromFileWhenImageIsLargeShouldDownsampleBitmap() { + val path = createTempImageFile(2000, 2000) + val bitmap = decodeSampledBitmapFromFile(path.absolutePathString(), 100, 100) + assertNotNull(bitmap) + assertTrue("Bitmap should be smaller than original", bitmap!!.width < 2000 && bitmap.height < 2000) + } + + @Test + fun testDecodeSampledBitmapFromFileWhenImageIsSmallerThanRequestedShouldKeepOriginalSize() { + val path = createTempImageFile(100, 100) + val bitmap = decodeSampledBitmapFromFile(path.absolutePathString(), 200, 200) + assertNotNull(bitmap) + assertEquals(100, bitmap!!.width) + assertEquals(100, bitmap.height) + } +} diff --git a/app/src/androidTest/java/com/nextcloud/extensions/BitmapRotationTests.kt b/app/src/androidTest/java/com/nextcloud/extensions/BitmapRotationTests.kt new file mode 100644 index 0000000..a2394f4 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/extensions/BitmapRotationTests.kt @@ -0,0 +1,90 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.extensions + +import android.graphics.Bitmap +import android.graphics.Color +import androidx.exifinterface.media.ExifInterface +import com.nextcloud.utils.rotateBitmapViaExif +import junit.framework.TestCase.assertEquals +import org.junit.Test + +class BitmapRotationTests { + + private fun createTestBitmap(): Bitmap = Bitmap.createBitmap(2, 2, Bitmap.Config.ARGB_8888).apply { + setPixel(0, 0, Color.RED) + setPixel(1, 0, Color.GREEN) + setPixel(0, 1, Color.BLUE) + setPixel(1, 1, Color.YELLOW) + } + + @Test + fun testRotateBitmapViaExifWhenGivenNullBitmapShouldReturnNull() { + val rotated = null.rotateBitmapViaExif(ExifInterface.ORIENTATION_ROTATE_90) + assertEquals(null, rotated) + } + + @Test + fun testRotateBitmapViaExifWhenGivenNormalOrientationShouldReturnSameBitmap() { + val bmp = createTestBitmap() + val rotated = bmp.rotateBitmapViaExif(ExifInterface.ORIENTATION_NORMAL) + assertEquals(bmp, rotated) + } + + @Test + fun testRotateBitmapViaExifWhenGivenRotate90ShouldReturnRotatedBitmap() { + val bmp = createTestBitmap() + val rotated = bmp.rotateBitmapViaExif(ExifInterface.ORIENTATION_ROTATE_90)!! + assertEquals(bmp.width, rotated.height) + assertEquals(bmp.height, rotated.width) + + assertEquals(Color.BLUE, rotated.getPixel(0, 0)) + assertEquals(Color.RED, rotated.getPixel(1, 0)) + assertEquals(Color.YELLOW, rotated.getPixel(0, 1)) + assertEquals(Color.GREEN, rotated.getPixel(1, 1)) + } + + @Test + fun testRotateBitmapViaExifWhenGivenRotate180ShouldReturnRotatedBitmap() { + val bmp = createTestBitmap() + val rotated = bmp.rotateBitmapViaExif(ExifInterface.ORIENTATION_ROTATE_180)!! + assertEquals(bmp.width, rotated.width) + assertEquals(bmp.height, rotated.height) + + assertEquals(Color.YELLOW, rotated.getPixel(0, 0)) + assertEquals(Color.BLUE, rotated.getPixel(1, 0)) + assertEquals(Color.GREEN, rotated.getPixel(0, 1)) + assertEquals(Color.RED, rotated.getPixel(1, 1)) + } + + @Test + fun testRotateBitmapViaExifWhenGivenFlipHorizontalShouldReturnFlippedBitmap() { + val bmp = createTestBitmap() + val rotated = bmp.rotateBitmapViaExif(ExifInterface.ORIENTATION_FLIP_HORIZONTAL)!! + assertEquals(bmp.width, rotated.width) + assertEquals(bmp.height, rotated.height) + + assertEquals(Color.GREEN, rotated.getPixel(0, 0)) + assertEquals(Color.RED, rotated.getPixel(1, 0)) + assertEquals(Color.YELLOW, rotated.getPixel(0, 1)) + assertEquals(Color.BLUE, rotated.getPixel(1, 1)) + } + + @Test + fun testRotateBitmapViaExifWhenGivenFlipVerticalShouldReturnFlippedBitmap() { + val bmp = createTestBitmap() + val rotated = bmp.rotateBitmapViaExif(ExifInterface.ORIENTATION_FLIP_VERTICAL)!! + assertEquals(bmp.width, rotated.width) + assertEquals(bmp.height, rotated.height) + + assertEquals(Color.BLUE, rotated.getPixel(0, 0)) + assertEquals(Color.YELLOW, rotated.getPixel(1, 0)) + assertEquals(Color.RED, rotated.getPixel(0, 1)) + assertEquals(Color.GREEN, rotated.getPixel(1, 1)) + } +} diff --git a/app/src/androidTest/java/com/nextcloud/extensions/BundleExtensionTests.kt b/app/src/androidTest/java/com/nextcloud/extensions/BundleExtensionTests.kt index 535986c..853a0e4 100644 --- a/app/src/androidTest/java/com/nextcloud/extensions/BundleExtensionTests.kt +++ b/app/src/androidTest/java/com/nextcloud/extensions/BundleExtensionTests.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2024 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.extensions diff --git a/app/src/androidTest/java/com/nextcloud/extensions/GetExifOrientationTests.kt b/app/src/androidTest/java/com/nextcloud/extensions/GetExifOrientationTests.kt new file mode 100644 index 0000000..3475c8e --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/extensions/GetExifOrientationTests.kt @@ -0,0 +1,78 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.extensions +import android.graphics.Bitmap +import android.graphics.Color +import androidx.exifinterface.media.ExifInterface +import com.nextcloud.utils.extensions.getExifOrientation +import junit.framework.TestCase.assertEquals +import org.junit.After +import org.junit.Test +import java.io.File + +class GetExifOrientationTests { + + private val tempFiles = mutableListOf() + + @Suppress("MagicNumber") + private fun createTempImageFile(): File { + val file = File.createTempFile("test_image", ".jpg") + tempFiles.add(file) + + val bmp = Bitmap.createBitmap(2, 2, Bitmap.Config.ARGB_8888).apply { + setPixel(0, 0, Color.RED) + setPixel(1, 0, Color.GREEN) + setPixel(0, 1, Color.BLUE) + setPixel(1, 1, Color.YELLOW) + } + + file.outputStream().use { out -> + bmp.compress(Bitmap.CompressFormat.JPEG, 100, out) + } + + return file + } + + @After + fun cleanup() { + tempFiles.forEach { it.delete() } + } + + @Test + fun testGetExifOrientationWhenExifIsRotate90ShouldReturnRotate90() { + val file = createTempImageFile() + + val exif = ExifInterface(file.absolutePath) + exif.setAttribute(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_ROTATE_90.toString()) + exif.saveAttributes() + + val orientation = getExifOrientation(file.absolutePath) + + assertEquals(ExifInterface.ORIENTATION_ROTATE_90, orientation) + } + + @Test + fun testGetExifOrientationWhenExifIsRotate180ShouldReturnRotate180() { + val file = createTempImageFile() + + val exif = ExifInterface(file.absolutePath) + exif.setAttribute(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_ROTATE_180.toString()) + exif.saveAttributes() + + val orientation = getExifOrientation(file.absolutePath) + assertEquals(ExifInterface.ORIENTATION_ROTATE_180, orientation) + } + + @Test + fun testGetExifOrientationWhenExifIsUndefinedShouldReturnUndefined() { + val file = createTempImageFile() + + val orientation = getExifOrientation(file.absolutePath) + assertEquals(ExifInterface.ORIENTATION_UNDEFINED, orientation) + } +} diff --git a/app/src/androidTest/java/com/nextcloud/extensions/IntentExtensionTests.kt b/app/src/androidTest/java/com/nextcloud/extensions/IntentExtensionTests.kt index 7a950a1..6fa385b 100644 --- a/app/src/androidTest/java/com/nextcloud/extensions/IntentExtensionTests.kt +++ b/app/src/androidTest/java/com/nextcloud/extensions/IntentExtensionTests.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2024 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.extensions diff --git a/app/src/androidTest/java/com/nextcloud/extensions/StringExtensionTests.kt b/app/src/androidTest/java/com/nextcloud/extensions/StringExtensionTests.kt new file mode 100644 index 0000000..36600e6 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/extensions/StringExtensionTests.kt @@ -0,0 +1,176 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.extensions +import com.nextcloud.utils.extensions.isNotBlankAndEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import org.junit.Test + +@Suppress("TooManyFunctions") +class StringExtensionTests { + @Test + fun testIsNotBlankAndEqualsWhenGivenBothStringsAreNull() { + val str1: String? = null + val str2: String? = null + assertFalse(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenFirstStringIsNull() { + val str1: String? = null + val str2 = "hello" + assertFalse(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenSecondStringIsNull() { + val str1 = "hello" + val str2: String? = null + assertFalse(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenBothStringsAreEmpty() { + val str1 = "" + val str2 = "" + assertFalse(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenFirstStringIsEmpty() { + val str1 = "" + val str2 = "hello" + assertFalse(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenSecondStringIsEmpty() { + val str1 = "hello" + val str2 = "" + assertFalse(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenBothStringsAreWhitespaceOnly() { + val str1 = " " + val str2 = " \t " + assertFalse(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenFirstStringIsWhitespaceOnly() { + val str1 = " " + val str2 = "hello" + assertFalse(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenSecondStringIsWhitespaceOnly() { + val str1 = "hello" + val str2 = " " + assertFalse(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenStringsAreDifferentButBothValid() { + val str1 = "hello" + val str2 = "world" + assertFalse(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenStringsHaveDifferentCase() { + val str1 = "Hello" + val str2 = "hello" + assertTrue(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenMixedCaseStrings() { + val str1 = "HeLLo WoRLd" + val str2 = "hello world" + assertTrue(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenUppercaseStrings() { + val str1 = "HELLO" + val str2 = "hello" + assertTrue(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenBothStringsAreIdenticalAndValid() { + val str1 = "hello" + val str2 = "hello" + assertTrue(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenBothStringsAreIdenticalWithSpaces() { + val str1 = "hello world" + val str2 = "hello world" + assertTrue(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenBothStringsAreIdenticalSingleCharacter() { + val str1 = "a" + val str2 = "A" + assertTrue(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenBothStringsAreIdenticalWithSpecialCharacters() { + val str1 = "hello@world!123" + val str2 = "HELLO@WORLD!123" + assertTrue(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenOneHasLeadingWhitespaceAndOtherDoesNot() { + val str1 = " hello" + val str2 = "HELLO" + assertFalse(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenOneHasTrailingWhitespaceAndOtherDoesNot() { + val str1 = "hello" + val str2 = "HELLO " + assertFalse(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenBothHaveIdenticalWhitespacePaddingDifferentCase() { + val str1 = " hello " + val str2 = " HELLO " + assertTrue(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenMixedWhitespaceCharacters() { + val str1 = "\t" + val str2 = "\n" + assertFalse(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenOneIsNullAndOtherIsEmpty() { + val str1: String? = null + val str2 = "" + assertFalse(str1.isNotBlankAndEquals(str2)) + } + + @Test + fun testIsNotBlankAndEqualsWhenGivenOneIsNullAndOtherIsWhitespace() { + val str1: String? = null + val str2 = " " + assertFalse(str1.isNotBlankAndEquals(str2)) + } +} diff --git a/app/src/androidTest/java/com/nextcloud/sso/InputStreamBinderTest.kt b/app/src/androidTest/java/com/nextcloud/sso/InputStreamBinderTest.kt index 83898d5..a7ad3de 100644 --- a/app/src/androidTest/java/com/nextcloud/sso/InputStreamBinderTest.kt +++ b/app/src/androidTest/java/com/nextcloud/sso/InputStreamBinderTest.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2021 Tobias Kaminsky * SPDX-FileCopyrightText: 2021 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.sso diff --git a/app/src/androidTest/java/com/nextcloud/test/GrantStoragePermissionRule.kt b/app/src/androidTest/java/com/nextcloud/test/GrantStoragePermissionRule.kt index 4876806..b310a89 100644 --- a/app/src/androidTest/java/com/nextcloud/test/GrantStoragePermissionRule.kt +++ b/app/src/androidTest/java/com/nextcloud/test/GrantStoragePermissionRule.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2021 Álvaro Brey * SPDX-FileCopyrightText: 2021 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.test diff --git a/app/src/androidTest/java/com/nextcloud/test/InjectionOverrideRule.kt b/app/src/androidTest/java/com/nextcloud/test/InjectionOverrideRule.kt index 3177c9f..0bb023f 100644 --- a/app/src/androidTest/java/com/nextcloud/test/InjectionOverrideRule.kt +++ b/app/src/androidTest/java/com/nextcloud/test/InjectionOverrideRule.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Álvaro Brey * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.test diff --git a/app/src/androidTest/java/com/nextcloud/test/InjectionTestActivityTest.kt b/app/src/androidTest/java/com/nextcloud/test/InjectionTestActivityTest.kt index d1294e2..76a3d19 100644 --- a/app/src/androidTest/java/com/nextcloud/test/InjectionTestActivityTest.kt +++ b/app/src/androidTest/java/com/nextcloud/test/InjectionTestActivityTest.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Álvaro Brey * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.test diff --git a/app/src/androidTest/java/com/nextcloud/test/RandomStringGenerator.kt b/app/src/androidTest/java/com/nextcloud/test/RandomStringGenerator.kt index 6bba3d6..44a55e3 100644 --- a/app/src/androidTest/java/com/nextcloud/test/RandomStringGenerator.kt +++ b/app/src/androidTest/java/com/nextcloud/test/RandomStringGenerator.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.test @@ -13,9 +13,7 @@ object RandomStringGenerator { @JvmOverloads @JvmStatic - fun make(length: Int = DEFAULT_LENGTH): String { - return (1..length) - .map { ALLOWED_CHARACTERS.random() } - .joinToString("") - } + fun make(length: Int = DEFAULT_LENGTH): String = (1..length) + .map { ALLOWED_CHARACTERS.random() } + .joinToString("") } diff --git a/app/src/androidTest/java/com/nextcloud/test/RetryTestRule.kt b/app/src/androidTest/java/com/nextcloud/test/RetryTestRule.kt index 720c514..45506db 100644 --- a/app/src/androidTest/java/com/nextcloud/test/RetryTestRule.kt +++ b/app/src/androidTest/java/com/nextcloud/test/RetryTestRule.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.test @@ -25,9 +25,7 @@ class RetryTestRule(val retryCount: Int = defaultRetryValue) : TestRule { private val defaultRetryValue: Int = if (BuildConfig.CI) 5 else 1 } - override fun apply(base: Statement, description: Description): Statement { - return statement(base, description) - } + override fun apply(base: Statement, description: Description): Statement = statement(base, description) @Suppress("TooGenericExceptionCaught") // and this exactly what we want here private fun statement(base: Statement, description: Description): Statement { diff --git a/app/src/androidTest/java/com/nextcloud/test/TestMainApp.kt b/app/src/androidTest/java/com/nextcloud/test/TestMainApp.kt index 398846a..912c7cd 100644 --- a/app/src/androidTest/java/com/nextcloud/test/TestMainApp.kt +++ b/app/src/androidTest/java/com/nextcloud/test/TestMainApp.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Álvaro Brey * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.test diff --git a/app/src/androidTest/java/com/nextcloud/test/model/TestModels.kt b/app/src/androidTest/java/com/nextcloud/test/model/TestModels.kt index 509777f..37dcd1d 100644 --- a/app/src/androidTest/java/com/nextcloud/test/model/TestModels.kt +++ b/app/src/androidTest/java/com/nextcloud/test/model/TestModels.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2024 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.test.model @@ -24,17 +24,11 @@ data class TestDataParcelable(val message: String) : Parcelable { parcel.writeString(message) } - override fun describeContents(): Int { - return 0 - } + override fun describeContents(): Int = 0 companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): TestDataParcelable { - return TestDataParcelable(parcel) - } + override fun createFromParcel(parcel: Parcel): TestDataParcelable = TestDataParcelable(parcel) - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } + override fun newArray(size: Int): Array = arrayOfNulls(size) } } diff --git a/app/src/androidTest/java/com/nextcloud/ui/BitmapIT.kt b/app/src/androidTest/java/com/nextcloud/ui/BitmapIT.kt index 94f636d..5bf8a8f 100644 --- a/app/src/androidTest/java/com/nextcloud/ui/BitmapIT.kt +++ b/app/src/androidTest/java/com/nextcloud/ui/BitmapIT.kt @@ -1,124 +1,80 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.ui import android.graphics.BitmapFactory import android.widget.ImageView import android.widget.LinearLayout -import androidx.test.espresso.intent.rule.IntentsTestRule +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot import com.nextcloud.test.TestActivity import com.owncloud.android.AbstractIT import com.owncloud.android.R import com.owncloud.android.utils.BitmapUtils +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.ScreenshotTest -import org.junit.Rule +import org.junit.After +import org.junit.Before import org.junit.Test class BitmapIT : AbstractIT() { - @get:Rule - val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false) + private val testClassName = "com.nextcloud.ui.BitmapIT" - @Test - @ScreenshotTest - fun roundBitmap() { - val file = getFile("christine.jpg") - val bitmap = BitmapFactory.decodeFile(file.absolutePath) - - val activity = testActivityRule.launchActivity(null) - val imageView = ImageView(activity).apply { - setImageBitmap(bitmap) - } - - val bitmap2 = BitmapFactory.decodeFile(file.absolutePath) - val imageView2 = ImageView(activity).apply { - setImageBitmap(BitmapUtils.roundBitmap(bitmap2)) - } - - val linearLayout = LinearLayout(activity).apply { - orientation = LinearLayout.VERTICAL - setBackgroundColor(context.getColor(R.color.grey_200)) - } - linearLayout.addView(imageView, 200, 200) - linearLayout.addView(imageView2, 200, 200) - activity.addView(linearLayout) - - screenshot(activity) + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) } - // @Test - // @ScreenshotTest - // fun glideSVG() { - // val activity = testActivityRule.launchActivity(null) - // val accountProvider = UserAccountManagerImpl.fromContext(activity) - // val clientFactory = ClientFactoryImpl(activity) - // - // val linearLayout = LinearLayout(activity).apply { - // orientation = LinearLayout.VERTICAL - // setBackgroundColor(context.getColor(R.color.grey_200)) - // } - // - // val file = getFile("christine.jpg") - // val bitmap = BitmapFactory.decodeFile(file.absolutePath) - // - // ImageView(activity).apply { - // setImageBitmap(bitmap) - // linearLayout.addView(this, 50, 50) - // } - // - // downloadIcon( - // client.baseUri.toString() + "/apps/files/img/app.svg", - // activity, - // linearLayout, - // accountProvider, - // clientFactory - // ) - // - // downloadIcon( - // client.baseUri.toString() + "/core/img/actions/group.svg", - // activity, - // linearLayout, - // accountProvider, - // clientFactory - // ) - // - // activity.addView(linearLayout) - // - // longSleep() - // - // screenshot(activity) - // } - // - // private fun downloadIcon( - // url: String, - // activity: TestActivity, - // linearLayout: LinearLayout, - // accountProvider: UserAccountManager, - // clientFactory: ClientFactory - // ) { - // val view = ImageView(activity).apply { - // linearLayout.addView(this, 50, 50) - // } - // val target = object : SimpleTarget() { - // override fun onResourceReady(resource: Drawable?, glideAnimation: GlideAnimation?) { - // view.setColorFilter(targetContext.getColor(R.color.dark), PorterDuff.Mode.SRC_ATOP) - // view.setImageDrawable(resource) - // } - // } - // - // testActivityRule.runOnUiThread { - // DisplayUtils.downloadIcon( - // accountProvider, - // clientFactory, - // activity, - // url, - // target, - // R.drawable.ic_user - // ) - // } - // } + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } + + @Test + @UiThread + @ScreenshotTest + fun roundBitmap() { + launchActivity().use { scenario -> + scenario.onActivity { activity -> + onIdleSync { + EspressoIdlingResource.increment() + val file = getFile("christine.jpg") + val bitmap = BitmapFactory.decodeFile(file.absolutePath) + + val imageView = ImageView(activity).apply { + setImageBitmap(bitmap) + } + + val bitmap2 = BitmapFactory.decodeFile(file.absolutePath) + val imageView2 = ImageView(activity).apply { + setImageBitmap(BitmapUtils.roundBitmap(bitmap2)) + } + + val linearLayout = LinearLayout(activity).apply { + orientation = LinearLayout.VERTICAL + setBackgroundColor(context.getColor(R.color.grey_200)) + } + linearLayout.addView(imageView, 200, 200) + linearLayout.addView(imageView2, 200, 200) + activity.addView(linearLayout) + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "roundBitmap", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(activity, screenShotName) + } + } + } + } } diff --git a/app/src/androidTest/java/com/nextcloud/ui/SetOnlineStatusBottomSheetIT.kt b/app/src/androidTest/java/com/nextcloud/ui/SetOnlineStatusBottomSheetIT.kt new file mode 100644 index 0000000..b799cab --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/ui/SetOnlineStatusBottomSheetIT.kt @@ -0,0 +1,56 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.nextcloud.ui + +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import com.owncloud.android.AbstractIT +import com.owncloud.android.lib.resources.users.Status +import com.owncloud.android.lib.resources.users.StatusType +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.utils.EspressoIdlingResource +import org.junit.After +import org.junit.Before +import org.junit.Test + +class SetOnlineStatusBottomSheetIT : AbstractIT() { + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } + + @Test + @UiThread + fun open() { + launchActivity().use { scenario -> + scenario.onActivity { activity -> + onIdleSync { + EspressoIdlingResource.increment() + val sut = SetOnlineStatusBottomSheet( + Status(StatusType.DND, "Working hard…", "🤖", -1) + ) + sut.show(activity.supportFragmentManager, "") + EspressoIdlingResource.decrement() + + onView(isRoot()).check(matches(isDisplayed())) + } + } + } + } +} diff --git a/app/src/androidTest/java/com/nextcloud/ui/SetStatusDialogFragmentIT.kt b/app/src/androidTest/java/com/nextcloud/ui/SetStatusDialogFragmentIT.kt deleted file mode 100644 index 4a75751..0000000 --- a/app/src/androidTest/java/com/nextcloud/ui/SetStatusDialogFragmentIT.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.nextcloud.ui - -import androidx.test.espresso.intent.rule.IntentsTestRule -import com.owncloud.android.AbstractIT -import com.owncloud.android.lib.resources.users.ClearAt -import com.owncloud.android.lib.resources.users.PredefinedStatus -import com.owncloud.android.lib.resources.users.Status -import com.owncloud.android.lib.resources.users.StatusType -import com.owncloud.android.ui.activity.FileDisplayActivity -import org.junit.Rule -import org.junit.Test - -class SetStatusDialogFragmentIT : AbstractIT() { - @get:Rule - var activityRule = IntentsTestRule(FileDisplayActivity::class.java, true, false) - - @Test - fun open() { - val sut = SetStatusDialogFragment.newInstance(user, Status(StatusType.DND, "Working hard…", "🤖", -1)) - val activity = activityRule.launchActivity(null) - - sut.show(activity.supportFragmentManager, "") - - val predefinedStatus: ArrayList = arrayListOf( - PredefinedStatus("meeting", "📅", "In a meeting", ClearAt("period", "3600")), - PredefinedStatus("commuting", "🚌", "Commuting", ClearAt("period", "1800")), - PredefinedStatus("remote-work", "🏡", "Working remotely", ClearAt("end-of", "day")), - PredefinedStatus("sick-leave", "🤒", "Out sick", ClearAt("end-of", "day")), - PredefinedStatus("vacationing", "🌴", "Vacationing", null) - ) - - shortSleep() - - activity.runOnUiThread { sut.setPredefinedStatus(predefinedStatus) } - - longSleep() - } -} diff --git a/app/src/androidTest/java/com/nextcloud/ui/SetStatusMessageBottomSheetIT.kt b/app/src/androidTest/java/com/nextcloud/ui/SetStatusMessageBottomSheetIT.kt new file mode 100644 index 0000000..9e2fc9c --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/ui/SetStatusMessageBottomSheetIT.kt @@ -0,0 +1,68 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.nextcloud.ui + +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import com.owncloud.android.AbstractIT +import com.owncloud.android.lib.resources.users.ClearAt +import com.owncloud.android.lib.resources.users.PredefinedStatus +import com.owncloud.android.lib.resources.users.Status +import com.owncloud.android.lib.resources.users.StatusType +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.utils.EspressoIdlingResource +import org.junit.After +import org.junit.Before +import org.junit.Test + +class SetStatusMessageBottomSheetIT : AbstractIT() { + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } + + @Test + @UiThread + fun open() { + launchActivity().use { scenario -> + scenario.onActivity { activity -> + onIdleSync { + EspressoIdlingResource.increment() + val sut = SetStatusMessageBottomSheet( + user, + Status(StatusType.DND, "Working hard…", "🤖", -1) + ) + sut.show(activity.supportFragmentManager, "") + val predefinedStatus: ArrayList = arrayListOf( + PredefinedStatus("meeting", "📅", "In a meeting", ClearAt("period", "3600")), + PredefinedStatus("commuting", "🚌", "Commuting", ClearAt("period", "1800")), + PredefinedStatus("be-right-back", "⏳", "Be right back", ClearAt("period", "900")), + PredefinedStatus("remote-work", "🏡", "Working remotely", ClearAt("end-of", "day")), + PredefinedStatus("sick-leave", "🤒", "Out sick", ClearAt("end-of", "day")), + PredefinedStatus("vacationing", "🌴", "Vacationing", null) + ) + sut.setPredefinedStatus(predefinedStatus) + EspressoIdlingResource.decrement() + + onView(isRoot()).check(matches(isDisplayed())) + } + } + } + } +} diff --git a/app/src/androidTest/java/com/nextcloud/utils/AutoRenameTests.kt b/app/src/androidTest/java/com/nextcloud/utils/AutoRenameTests.kt new file mode 100644 index 0000000..68e99b9 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/utils/AutoRenameTests.kt @@ -0,0 +1,253 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils + +import com.nextcloud.utils.autoRename.AutoRename +import com.owncloud.android.AbstractOnServerIT +import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile +import com.owncloud.android.lib.resources.status.CapabilityBooleanType +import com.owncloud.android.lib.resources.status.NextcloudVersion +import com.owncloud.android.lib.resources.status.OCCapability +import org.junit.Before +import org.junit.Test + +@Suppress("TooManyFunctions") +class AutoRenameTests : AbstractOnServerIT() { + + private var capability: OCCapability = fileDataStorageManager.getCapability(account.name) + private val forbiddenFilenameExtension = "." + private val forbiddenFilenameCharacter = ">" + + @Before + fun setup() { + testOnlyOnServer(NextcloudVersion.nextcloud_30) + + capability = capability.apply { + isWCFEnabled = CapabilityBooleanType.TRUE + forbiddenFilenameExtensionJson = listOf( + """[" ",".",".part",".part"]""", + """[".",".part",".part"," "]""", + """[".",".part"," ", ".part"]""", + """[".part"," ", ".part","."]""", + """[" ",".",".PART",".PART"]""", + """[".",".PART",".PART"," "]""", + """[".",".PART"," ", ".PART"]""", + """[".PART"," ", ".PART","."]""" + ).random() + forbiddenFilenameCharactersJson = """["<", ">", ":", "\\\\", "/", "|", "?", "*", "&"]""" + } + } + + @Test + fun testInvalidChar() { + val filename = "file${forbiddenFilenameCharacter}file.txt" + val result = AutoRename.rename(filename, capability) + val expectedFilename = "file_file.txt" + assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } + } + + @Test + fun testInvalidExtension() { + val filename = "file$forbiddenFilenameExtension" + val result = AutoRename.rename(filename, capability) + val expectedFilename = "file_" + assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } + } + + @Test + fun testMultipleInvalidChars() { + val filename = "file|name?<>.txt" + val result = AutoRename.rename(filename, capability) + val expectedFilename = "file_name___.txt" + assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } + } + + @Test + fun testStartEndInvalidExtensions() { + val filename = " .file.part " + val result = AutoRename.rename(filename, capability) + val expectedFilename = "_file_part" + assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } + } + + @Test + fun testStartInvalidExtension() { + val filename = " .file.part" + val result = AutoRename.rename(filename, capability) + val expectedFilename = "_file_part" + assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } + } + + @Test + fun testEndInvalidExtension() { + val filename = ".file.part " + val result = AutoRename.rename(filename, capability) + val expectedFilename = "_file_part" + assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } + } + + @Test + fun testMiddleNonPrintableChar() { + val filename = "file\u0001name.txt" + val result = AutoRename.rename(filename, capability) + val expectedFilename = "filename.txt" + assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } + } + + @Test + fun testStartNonPrintableChar() { + val filename = "\u0001filename.txt" + val result = AutoRename.rename(filename, capability) + val expectedFilename = "filename.txt" + assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } + } + + @Test + fun testEndNonPrintableChar() { + val filename = "filename.txt\u0001" + val result = AutoRename.rename(filename, capability) + val expectedFilename = "filename.txt" + assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } + } + + @Test + fun testExtensionNonPrintableChar() { + val filename = "filename.t\u0001xt" + val result = AutoRename.rename(filename, capability) + val expectedFilename = "filename.txt" + assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } + } + + @Test + fun testMiddleInvalidFolderChar() { + val folderPath = "abc/def/kg$forbiddenFilenameCharacter/lmo/pp/" + val result = AutoRename.rename(folderPath, capability) + val expectedFolderName = "abc/def/kg_/lmo/pp/" + assert(result == expectedFolderName) { "Expected $expectedFolderName but got $result" } + } + + @Test + fun testEndInvalidFolderChar() { + val folderPath = "abc/def/kg/lmo/pp$forbiddenFilenameCharacter/" + val result = AutoRename.rename(folderPath, capability) + val expectedFolderName = "abc/def/kg/lmo/pp_/" + assert(result == expectedFolderName) { "Expected $expectedFolderName but got $result" } + } + + @Test + fun testStartInvalidFolderChar() { + val folderPath = "${forbiddenFilenameCharacter}abc/def/kg/lmo/pp/" + val result = AutoRename.rename(folderPath, capability) + val expectedFolderName = "_abc/def/kg/lmo/pp/" + assert(result == expectedFolderName) { "Expected $expectedFolderName but got $result" } + } + + @Test + fun testMixedInvalidChar() { + val filename = " file\u0001na${forbiddenFilenameCharacter}me.txt " + val result = AutoRename.rename(filename, capability) + val expectedFilename = "filena_me.txt" + assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } + } + + @Test + fun testStartsWithPathSeparator() { + val folderPath = "/abc/def/kg/lmo/pp$forbiddenFilenameCharacter/file.txt/" + val result = AutoRename.rename(folderPath, capability) + val expectedFolderName = "/abc/def/kg/lmo/pp_/file.txt/" + assert(result == expectedFolderName) { "Expected $expectedFolderName but got $result" } + } + + @Test + fun testStartsWithPathSeparatorAndValidFilepath() { + val folderPath = "/COm02/2569.webp/" + val result = AutoRename.rename(folderPath, capability) + val expectedFolderName = "/COm02/2569.webp/" + assert(result == expectedFolderName) { "Expected $expectedFolderName but got $result" } + } + + @Test + fun testValidFilename() { + val filename = ".file.TXT" + val result = AutoRename.rename(filename, capability) + val expectedFilename = "_file.txt" + assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } + } + + @Test + fun testRenameExtensionForFolder() { + val filename = "/Pictures/@User/SubDir/08.16.07 Ka Yel/" + val result = AutoRename.rename(filename, capability) + assert(result == filename) { "Expected $filename but got $result" } + } + + @Test + fun testRenameExtensionForFile() { + val filename = "/Pictures/@User/SubDir/08.16.07 Ka Yel.TXT" + val result = AutoRename.rename(filename, capability) + val expectedFilename = "/Pictures/@User/SubDir/08.16.07 Ka Yel.txt" + assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } + } + + @Test + fun testE2EEFile() { + val decryptedFile = DecryptedFile( + authenticationTag = "HQlWBdm+gYC5kZwWnqXR1Q==", + filename = "a:a.jpg", + nonce = "sigyys8SfPZSScDJ860vYw==", + mimetype = "image/jpeg", + key = "sigyys8SfPZSScDJ860vYw==" + ) + + val result = AutoRename.rename(decryptedFile.filename, capability) + val expectedFilename = "a_a.jpg" + assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } + } + + @Test + fun testRemovingLeadingWhitespace() { + val filename = " readme.txt" + val result = AutoRename.rename(filename, capability) + val expectedFilename = "readme.txt" + assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } + } + + @Test + fun testRemovingTrailingWhitespace() { + val filename = "readme.txt " + val result = AutoRename.rename(filename, capability) + val expectedFilename = "readme.txt" + assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } + } + + @Test + fun testRemovingTrailingAndLeadingWhitespace() { + val filename = " readme.txt " + val result = AutoRename.rename(filename, capability) + val expectedFilename = "readme.txt" + assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } + } + + @Test + fun testFolderNameLowercase() { + val filename = "Foo.Bar.Baz" + val result = AutoRename.rename(filename, capability, isFolderPath = true) + val expectedFilename = "Foo.Bar.Baz" + assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } + } + + @Test + fun skipAutoRenameWhenWCFDisabled() { + capability = capability.apply { + isWCFEnabled = CapabilityBooleanType.FALSE + } + val filename = " readme.txt " + val result = AutoRename.rename(filename, capability, isFolderPath = true) + assert(result == filename) { "Expected $filename but got $result" } + } +} diff --git a/app/src/androidTest/java/com/nextcloud/utils/CertificateValidatorTests.kt b/app/src/androidTest/java/com/nextcloud/utils/CertificateValidatorTests.kt new file mode 100644 index 0000000..1278585 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/utils/CertificateValidatorTests.kt @@ -0,0 +1,45 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils + +import androidx.test.platform.app.InstrumentationRegistry +import com.google.gson.Gson +import com.owncloud.android.datamodel.Credentials +import com.owncloud.android.ui.dialog.setupEncryption.CertificateValidator +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.io.InputStreamReader + +class CertificateValidatorTests { + + private var sut: CertificateValidator? = null + + @Before + fun setup() { + sut = CertificateValidator() + } + + @After + fun destroy() { + sut = null + } + + @Test + fun testValidateWhenGivenValidServerKeyAndCertificateShouldReturnTrue() { + val inputStream = + InstrumentationRegistry.getInstrumentation().context.assets.open("credentials.json") + + val credentials = InputStreamReader(inputStream).use { reader -> + Gson().fromJson(reader, Credentials::class.java) + } + + val isCertificateValid = sut?.validate(credentials.publicKey, credentials.certificate) ?: false + assert(isCertificateValid) + } +} diff --git a/app/src/androidTest/java/com/nextcloud/utils/CheckWCFRestrictionsTests.kt b/app/src/androidTest/java/com/nextcloud/utils/CheckWCFRestrictionsTests.kt new file mode 100644 index 0000000..c16e705 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/utils/CheckWCFRestrictionsTests.kt @@ -0,0 +1,63 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.nextcloud.utils + +import com.nextcloud.utils.extensions.checkWCFRestrictions +import com.owncloud.android.lib.resources.status.CapabilityBooleanType +import com.owncloud.android.lib.resources.status.NextcloudVersion +import com.owncloud.android.lib.resources.status.OCCapability +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +@Suppress("MagicNumber") +class CheckWCFRestrictionsTests { + + private fun createCapability( + version: NextcloudVersion, + isWCFEnabled: CapabilityBooleanType = CapabilityBooleanType.UNKNOWN + ): OCCapability = OCCapability().apply { + this.versionMayor = version.majorVersionNumber + this.isWCFEnabled = isWCFEnabled + } + + @Test + fun testReturnsFalseForVersionsOlderThan30() { + val capability = createCapability(NextcloudVersion.nextcloud_29) + assertFalse(capability.checkWCFRestrictions()) + } + + @Test + fun testReturnsTrueForVersion30WhenWCFAlwaysEnabled() { + val capability = createCapability(NextcloudVersion.nextcloud_30) + assertTrue(capability.checkWCFRestrictions()) + } + + @Test + fun testReturnsTrueForVersion31WhenWCFAlwaysEnabled() { + val capability = createCapability(NextcloudVersion.nextcloud_31) + assertTrue(capability.checkWCFRestrictions()) + } + + @Test + fun testReturnsTrueForVersion32WhenWCFEnabled() { + val capability = createCapability(NextcloudVersion.nextcloud_32, CapabilityBooleanType.TRUE) + assertTrue(capability.checkWCFRestrictions()) + } + + @Test + fun testReturnsFalseForVersion32WhenWCFDisabled() { + val capability = createCapability(NextcloudVersion.nextcloud_32, CapabilityBooleanType.FALSE) + assertFalse(capability.checkWCFRestrictions()) + } + + @Test + fun testReturnsFalseForVersion32WhenWCFIsUnknown() { + val capability = createCapability(NextcloudVersion.nextcloud_32) + assertFalse(capability.checkWCFRestrictions()) + } +} diff --git a/app/src/androidTest/java/com/nextcloud/utils/FileHelperTest.kt b/app/src/androidTest/java/com/nextcloud/utils/FileHelperTest.kt new file mode 100644 index 0000000..8f01315 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/utils/FileHelperTest.kt @@ -0,0 +1,204 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.nextcloud.utils + +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.io.File +import java.nio.file.Files + +@Suppress("TooManyFunctions") +class FileHelperTest { + + private lateinit var testDirectory: File + + @Before + fun setup() { + testDirectory = Files.createTempDirectory("test").toFile() + } + + @After + fun tearDown() { + testDirectory.deleteRecursively() + } + + @Test + fun testListDirectoryEntriesWhenGivenNullDirectoryShouldReturnEmptyList() { + val result = FileHelper.listDirectoryEntries(null, 0, 10, false) + assertTrue(result.isEmpty()) + } + + @Test + fun testListDirectoryEntriesWhenGivenNonExistentDirectoryShouldReturnEmptyList() { + val nonExistent = File(testDirectory, "does_not_exist") + val result = FileHelper.listDirectoryEntries(nonExistent, 0, 10, false) + assertTrue(result.isEmpty()) + } + + @Test + fun testListDirectoryEntriesWhenGivenFileInsteadOfDirectoryShouldReturnEmptyList() { + val file = File(testDirectory, "test.txt") + file.createNewFile() + val result = FileHelper.listDirectoryEntries(file, 0, 10, false) + assertTrue(result.isEmpty()) + } + + @Test + fun testListDirectoryEntriesWhenGivenEmptyDirectoryShouldReturnEmptyList() { + val result = FileHelper.listDirectoryEntries(testDirectory, 0, 10, false) + assertTrue(result.isEmpty()) + } + + @Test + fun testListDirectoryEntriesWhenFetchingFoldersShouldReturnOnlyFolders() { + File(testDirectory, "folder1").mkdir() + File(testDirectory, "folder2").mkdir() + File(testDirectory, "file1.txt").createNewFile() + File(testDirectory, "file2.txt").createNewFile() + + val result = FileHelper.listDirectoryEntries(testDirectory, 0, 10, true) + + assertEquals(2, result.size) + assertTrue(result.all { it.isDirectory }) + } + + @Test + fun testListDirectoryEntriesWhenFetchingFilesShouldReturnOnlyFiles() { + File(testDirectory, "folder1").mkdir() + File(testDirectory, "folder2").mkdir() + File(testDirectory, "file1.txt").createNewFile() + File(testDirectory, "file2.txt").createNewFile() + + val result = FileHelper.listDirectoryEntries(testDirectory, 0, 10, false) + + assertEquals(2, result.size) + assertTrue(result.all { it.isFile }) + } + + @Test + fun testListDirectoryEntriesWhenStartIndexProvidedShouldSkipCorrectNumberOfItems() { + for (i in 1..5) File(testDirectory, "file$i.txt").createNewFile() + val result = FileHelper.listDirectoryEntries(testDirectory, 2, 10, false) + assertEquals(3, result.size) + } + + @Test + fun testListDirectoryEntriesWhenMaxItemsProvidedShouldLimitResults() { + for (i in 1..10) File(testDirectory, "file$i.txt").createNewFile() + val result = FileHelper.listDirectoryEntries(testDirectory, 0, 5, false) + assertEquals(5, result.size) + } + + @Test + fun testListDirectoryEntriesWhenGivenStartIndexAndMaxItemsShouldReturnCorrectSubset() { + for (i in 1..10) File(testDirectory, "file$i.txt").createNewFile() + val result = FileHelper.listDirectoryEntries(testDirectory, 3, 4, false) + assertEquals(4, result.size) + } + + @Test + fun testListDirectoryEntriesWhenStartIndexBeyondAvailableShouldReturnEmptyList() { + for (i in 1..3) File(testDirectory, "file$i.txt").createNewFile() + val result = FileHelper.listDirectoryEntries(testDirectory, 10, 5, false) + assertTrue(result.isEmpty()) + } + + @Test + fun testListDirectoryEntriesWhenMaxItemsBeyondAvailableShouldReturnAllItems() { + for (i in 1..3) File(testDirectory, "file$i.txt").createNewFile() + val result = FileHelper.listDirectoryEntries(testDirectory, 0, 100, false) + assertEquals(3, result.size) + } + + @Test + fun testListDirectoryEntriesWhenFetchingFoldersWithOffsetShouldSkipCorrectly() { + for (i in 1..5) File(testDirectory, "folder$i").mkdir() + for (i in 1..3) File(testDirectory, "file$i.txt").createNewFile() + + val result = FileHelper.listDirectoryEntries(testDirectory, 2, 10, true) + + assertEquals(3, result.size) + assertTrue(result.all { it.isDirectory }) + } + + @Test + fun testListDirectoryEntriesWhenFetchingFilesWithOffsetShouldSkipCorrectly() { + for (i in 1..3) File(testDirectory, "folder$i").mkdir() + for (i in 1..5) File(testDirectory, "file$i.txt").createNewFile() + + val result = FileHelper.listDirectoryEntries(testDirectory, 2, 10, false) + + assertEquals(3, result.size) + assertTrue(result.all { it.isFile }) + } + + @Test + fun testListDirectoryEntriesWhenGivenOnlyFoldersAndFetchingFilesShouldReturnEmptyList() { + for (i in 1..5) File(testDirectory, "folder$i").mkdir() + val result = FileHelper.listDirectoryEntries(testDirectory, 0, 10, false) + assertTrue(result.isEmpty()) + } + + @Test + fun testListDirectoryEntriesWhenGivenOnlyFilesAndFetchingFoldersShouldReturnEmptyList() { + for (i in 1..5) File(testDirectory, "file$i.txt").createNewFile() + val result = FileHelper.listDirectoryEntries(testDirectory, 0, 10, true) + assertTrue(result.isEmpty()) + } + + @Test + fun testListDirectoryEntriesWhenMaxItemsIsZeroShouldReturnEmptyList() { + for (i in 1..5) File(testDirectory, "file$i.txt").createNewFile() + val result = FileHelper.listDirectoryEntries(testDirectory, 0, 0, false) + assertTrue(result.isEmpty()) + } + + @Test + fun testListDirectoryEntriesWhenGivenMixedContentShouldFilterCorrectly() { + for (i in 1..3) File(testDirectory, "folder$i").mkdir() + for (i in 1..7) File(testDirectory, "file$i.txt").createNewFile() + + val folders = FileHelper.listDirectoryEntries(testDirectory, 0, 10, true) + val files = FileHelper.listDirectoryEntries(testDirectory, 0, 10, false) + + assertEquals(3, folders.size) + assertEquals(7, files.size) + assertTrue(folders.all { it.isDirectory }) + assertTrue(files.all { it.isFile }) + } + + @Test + fun testListDirectoryEntriesWhenPaginatingFoldersShouldWorkCorrectly() { + for (i in 1..10) File(testDirectory, "folder$i").mkdir() + + val page1 = FileHelper.listDirectoryEntries(testDirectory, 0, 3, true) + val page2 = FileHelper.listDirectoryEntries(testDirectory, 3, 3, true) + val page3 = FileHelper.listDirectoryEntries(testDirectory, 6, 3, true) + val page4 = FileHelper.listDirectoryEntries(testDirectory, 9, 3, true) + + assertEquals(3, page1.size) + assertEquals(3, page2.size) + assertEquals(3, page3.size) + assertEquals(1, page4.size) + } + + @Test + fun testListDirectoryEntriesWhenPaginatingFilesShouldWorkCorrectly() { + for (i in 1..10) File(testDirectory, "file$i.txt").createNewFile() + + val page1 = FileHelper.listDirectoryEntries(testDirectory, 0, 4, false) + val page2 = FileHelper.listDirectoryEntries(testDirectory, 4, 4, false) + val page3 = FileHelper.listDirectoryEntries(testDirectory, 8, 4, false) + + assertEquals(4, page1.size) + assertEquals(4, page2.size) + assertEquals(2, page3.size) + } +} diff --git a/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt b/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt new file mode 100644 index 0000000..89d4bb3 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/utils/FileNameValidatorTests.kt @@ -0,0 +1,243 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils + +import com.nextcloud.utils.fileNameValidator.FileNameValidator +import com.owncloud.android.AbstractOnServerIT +import com.owncloud.android.R +import com.owncloud.android.lib.resources.status.CapabilityBooleanType +import com.owncloud.android.lib.resources.status.NextcloudVersion +import com.owncloud.android.lib.resources.status.OCCapability +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +@Suppress("TooManyFunctions") +class FileNameValidatorTests : AbstractOnServerIT() { + + private var capability: OCCapability = fileDataStorageManager.getCapability(account.name) + + @Before + fun setup() { + capability = capability.apply { + isWCFEnabled = CapabilityBooleanType.TRUE + forbiddenFilenamesJson = """[".htaccess",".htaccess"]""" + forbiddenFilenameBaseNamesJson = """ + ["con", "prn", "aux", "nul", "com0", "com1", "com2", "com3", "com4", + "com5", "com6", "com7", "com8", "com9", "com¹", "com²", "com³", + "lpt0", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", + "lpt8", "lpt9", "lpt¹", "lpt²", "lpt³"] + """ + forbiddenFilenameExtensionJson = """[" ",".",".part",".part"]""" + forbiddenFilenameCharactersJson = """["<", ">", ":", "\\\\", "/", "|", "?", "*", "&"]""" + } + } + + @Test + fun testInvalidCharacter() { + testOnlyOnServer(NextcloudVersion.nextcloud_30) + + val result = FileNameValidator.checkFileName("file + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils + +import com.owncloud.android.datamodel.quickPermission.QuickPermissionType +import com.owncloud.android.lib.resources.shares.OCShare +import com.owncloud.android.lib.resources.shares.ShareType +import com.owncloud.android.lib.resources.shares.extensions.isAllowDownloadAndSyncEnabled +import com.owncloud.android.lib.resources.shares.extensions.toggleAllowDownloadAndSync +import com.owncloud.android.ui.fragment.util.SharePermissionManager +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import org.junit.Test + +@Suppress("TooManyFunctions") +class SharePermissionManagerTest { + + private fun createShare(sharePermission: Int, isFolder: Boolean = false, attributesJson: String? = null): OCShare = + if (isFolder) { + OCShare("/test") + .apply { + permissions = sharePermission + attributes = attributesJson + shareType = ShareType.INTERNAL + sharedDate = 1188206955 + shareWith = "User 1" + sharedWithDisplayName = "User 1" + } + } else { + OCShare("/test.png") + .apply { + permissions = sharePermission + attributes = attributesJson + shareType = ShareType.INTERNAL + sharedDate = 1188206955 + shareWith = "User 1" + sharedWithDisplayName = "User 1" + } + }.apply { + this.isFolder = isFolder + } + + // region Permission change tests + @Test + fun testTogglePermissionShouldAddPermissionFlagWhenChecked() { + val initialPermission = OCShare.READ_PERMISSION_FLAG + val updatedPermission = + SharePermissionManager.togglePermission(true, initialPermission, OCShare.UPDATE_PERMISSION_FLAG) + val updatedShare = createShare(updatedPermission) + assertTrue(SharePermissionManager.isCustomPermission(updatedShare)) + } + + @Test + fun testTogglePermissionShouldRemovePermissionFlagWhenUnchecked() { + val initialPermission = OCShare.READ_PERMISSION_FLAG + OCShare.UPDATE_PERMISSION_FLAG + val updatedPermission = + SharePermissionManager.togglePermission(false, initialPermission, OCShare.UPDATE_PERMISSION_FLAG) + val updatedShare = createShare(updatedPermission) + assertTrue(SharePermissionManager.isViewOnly(updatedShare)) + } + // endregion + + // region HasPermissions tests + @Test + fun testHasPermissionShouldReturnTrueIfPermissionPresent() { + val permission = OCShare.READ_PERMISSION_FLAG + OCShare.UPDATE_PERMISSION_FLAG + assertTrue(SharePermissionManager.hasPermission(permission, OCShare.UPDATE_PERMISSION_FLAG)) + } + + @Test + fun testHasPermissionShouldReturnFalseIfPermissionNotPresent() { + val permission = OCShare.READ_PERMISSION_FLAG + assertFalse(SharePermissionManager.hasPermission(permission, OCShare.UPDATE_PERMISSION_FLAG)) + } + // endregion + + // region Helper Method Tests + @Test + fun testCanEditShouldReturnTrueIfAllPermissionsPresent() { + val share = createShare(OCShare.MAXIMUM_PERMISSIONS_FOR_FOLDER, isFolder = true) + assertTrue(SharePermissionManager.canEdit(share)) + } + + @Test + fun testCanEditShouldReturnFalseIfPermissionsAreInsufficient() { + val share = createShare(OCShare.READ_PERMISSION_FLAG) + assertFalse(SharePermissionManager.canEdit(share)) + } + + @Test + fun testIsViewOnlyShouldReturnTrueIfOnlyReadPermissionSet() { + val share = createShare(OCShare.READ_PERMISSION_FLAG) + assertTrue(SharePermissionManager.isViewOnly(share)) + } + + @Test + fun testIsFileRequestShouldReturnTrueIfOnlyCreatePermissionSetOnFolder() { + val share = createShare(OCShare.CREATE_PERMISSION_FLAG, isFolder = true) + assertTrue(SharePermissionManager.isFileRequest(share)) + } + + @Test + fun testIsFileRequestShouldReturnFalseIfOnlyCreatePermissionSetOnFile() { + val share = createShare(OCShare.CREATE_PERMISSION_FLAG) + assertFalse(SharePermissionManager.isFileRequest(share)) + } + + @Test + fun testIsSecureFileDropShouldReturnTrueIfReadAndCreatePermissionsPresent() { + val permission = OCShare.READ_PERMISSION_FLAG + OCShare.CREATE_PERMISSION_FLAG + val share = createShare(permission) + assertTrue(SharePermissionManager.isSecureFileDrop(share)) + } + + @Test + fun testCanReshareShouldReturnTrueIfSharePermissionIsPresent() { + val share = createShare(OCShare.SHARE_PERMISSION_FLAG) + assertTrue(SharePermissionManager.canReshare(share)) + } + + @Test + fun testGetMaximumPermissionForFolder() { + assertEquals( + OCShare.MAXIMUM_PERMISSIONS_FOR_FOLDER, + SharePermissionManager.getMaximumPermission(isFolder = true) + ) + } + + @Test + fun testGetMaximumPermissionForFile() { + assertEquals( + OCShare.MAXIMUM_PERMISSIONS_FOR_FILE, + SharePermissionManager.getMaximumPermission(isFolder = false) + ) + } + // endregion + + // region GetSelectedTypeTests + @Test + fun testGetSelectedTypeShouldReturnCanEditWhenFullPermissionsGiven() { + val share = createShare(OCShare.MAXIMUM_PERMISSIONS_FOR_FILE) + assertEquals(QuickPermissionType.CAN_EDIT, SharePermissionManager.getSelectedType(share, encrypted = false)) + } + + @Test + fun testGetSelectedTypeShouldReturnSecureFileDropWhenEncryptedAndReadCreateGiven() { + val permission = OCShare.READ_PERMISSION_FLAG + OCShare.CREATE_PERMISSION_FLAG + val share = createShare(permission) + assertEquals( + QuickPermissionType.SECURE_FILE_DROP, + SharePermissionManager.getSelectedType(share, encrypted = true) + ) + } + + @Test + fun testGetSelectedTypeShouldReturnFileRequestWhenCreatePermissionGiven() { + val share = createShare(OCShare.CREATE_PERMISSION_FLAG, isFolder = true) + assertEquals(QuickPermissionType.FILE_REQUEST, SharePermissionManager.getSelectedType(share, encrypted = false)) + } + + @Test + fun testGetSelectedTypeShouldReturnViewOnlyWhenReadPermissionGiven() { + val share = createShare(OCShare.READ_PERMISSION_FLAG) + assertEquals(QuickPermissionType.VIEW_ONLY, SharePermissionManager.getSelectedType(share, encrypted = false)) + } + + @Test + fun testGetSelectedTypeShouldReturnCustomPermissionOnlyWhenCustomPermissionGiven() { + val share = createShare(OCShare.READ_PERMISSION_FLAG + OCShare.UPDATE_PERMISSION_FLAG) + assertEquals( + QuickPermissionType.CUSTOM_PERMISSIONS, + SharePermissionManager.getSelectedType(share, encrypted = false) + ) + } + + @Test + fun testGetSelectedTypeShouldReturnNoneOnlyWhenNoPermissionGiven() { + val share = createShare(OCShare.NO_PERMISSION) + assertEquals( + QuickPermissionType.NONE, + SharePermissionManager.getSelectedType(share, encrypted = false) + ) + } + // endregion + + // region CustomPermissions Tests + @Test + fun testIsCustomPermissionShouldReturnFalseWhenNoPermissionsGiven() { + val permission = OCShare.NO_PERMISSION + val share = createShare(permission, isFolder = false) + assertFalse(SharePermissionManager.isCustomPermission(share)) + } + + @Test + fun testIsCustomPermissionShouldReturnFalseWhenNoReadPermissionsGiven() { + val permission = OCShare.SHARE_PERMISSION_FLAG + OCShare.UPDATE_PERMISSION_FLAG + val share = createShare(permission, isFolder = false) + assertFalse(SharePermissionManager.isCustomPermission(share)) + } + + @Test + fun testIsCustomPermissionShouldReturnTrueWhenUpdatePermissionsGivenOnFile() { + val permission = OCShare.READ_PERMISSION_FLAG + OCShare.UPDATE_PERMISSION_FLAG + val share = createShare(permission, isFolder = false) + assertTrue(SharePermissionManager.isCustomPermission(share)) + } + + @Test + fun testIsCustomPermissionShouldReturnTrueWhenUpdateAndSharePermissionsGivenOnFile() { + val permission = OCShare.READ_PERMISSION_FLAG + OCShare.UPDATE_PERMISSION_FLAG + OCShare.SHARE_PERMISSION_FLAG + val share = createShare(permission, isFolder = false) + assertTrue(SharePermissionManager.isCustomPermission(share)) + } + + @Test + fun testIsCustomPermissionShouldReturnFalseWhenCreatePermissionsGivenOnFile() { + val permission = OCShare.READ_PERMISSION_FLAG + OCShare.CREATE_PERMISSION_FLAG + val share = createShare(permission, isFolder = false) + assertFalse(SharePermissionManager.isCustomPermission(share)) + } + + @Test + fun testIsCustomPermissionShouldReturnFalseWhenDeletePermissionsGivenOnFile() { + val permission = OCShare.READ_PERMISSION_FLAG + OCShare.DELETE_PERMISSION_FLAG + val share = createShare(permission, isFolder = false) + assertFalse(SharePermissionManager.isCustomPermission(share)) + } + + @Test + fun testIsCustomPermissionShouldReturnTrueWhenCreatePermissionsGivenOnFolder() { + val permission = OCShare.READ_PERMISSION_FLAG + OCShare.CREATE_PERMISSION_FLAG + val share = createShare(permission, isFolder = true) + assertTrue(SharePermissionManager.isCustomPermission(share)) + } + + @Test + fun testIsCustomPermissionShouldReturnTrueWhenMixedPermissionsOnFile() { + val permission = OCShare.READ_PERMISSION_FLAG + OCShare.UPDATE_PERMISSION_FLAG + val share = createShare(permission, isFolder = false) + assertTrue(SharePermissionManager.isCustomPermission(share)) + } + // endregion + + // region Attributes Tests + @Test + fun testToggleAllowDownloadAndSyncShouldCreateAttributeJsonIfNoneExists() { + val ocShare = OCShare().apply { + isFolder = true + shareType = ShareType.USER + permissions = 17 + } + ocShare.attributes = toggleAllowDownloadAndSync( + ocShare.attributes, + isChecked = true, + useV2DownloadAttributes = false + ) + assertTrue(ocShare.isAllowDownloadAndSyncEnabled(false)) + } + + @Test + fun testIsAllowDownloadAndSyncEnabledShouldReturnFalseIfAttributeIsMissing() { + val share = createShare(OCShare.READ_PERMISSION_FLAG, attributesJson = null) + assertFalse(share.isAllowDownloadAndSyncEnabled(false)) + } + // endregion +} diff --git a/app/src/androidTest/java/com/nmc/android/ui/LauncherActivityIT.kt b/app/src/androidTest/java/com/nmc/android/ui/LauncherActivityIT.kt index 3523aec..921fd19 100644 --- a/app/src/androidTest/java/com/nmc/android/ui/LauncherActivityIT.kt +++ b/app/src/androidTest/java/com/nmc/android/ui/LauncherActivityIT.kt @@ -2,51 +2,76 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2023 TSI-mc - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nmc.android.ui +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import com.owncloud.android.AbstractIT import com.owncloud.android.R -import org.junit.Rule +import com.owncloud.android.utils.EspressoIdlingResource +import org.junit.After +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class LauncherActivityIT : AbstractIT() { - @get:Rule - val activityRule = ActivityScenarioRule(LauncherActivity::class.java) + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } - @Test - fun testSplashScreenWithEmptyTitlesShouldHideTitles() { - waitForIdleSync() - - onView(withId(R.id.ivSplash)).check(matches(isCompletelyDisplayed())) - - onView(withId(R.id.splashScreenBold)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))) - onView(withId(R.id.splashScreenNormal)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))) + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) } @Test - fun testSplashScreenWithTitlesShouldShowTitles() { - waitForIdleSync() - onView(withId(R.id.ivSplash)).check(matches(isCompletelyDisplayed())) - - activityRule.scenario.onActivity { - it.setSplashTitles("Example", "Cloud") + @UiThread + fun testSplashScreenWithEmptyTitlesShouldHideTitles() { + launchActivity().use { scenario -> + scenario.onActivity { _ -> + onIdleSync { + onView(withId(R.id.ivSplash)).check(matches(isCompletelyDisplayed())) + onView( + withId(R.id.splashScreenBold) + ).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))) + onView( + withId(R.id.splashScreenNormal) + ).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))) + } + } } + } - val onePercentArea = ViewMatchers.isDisplayingAtLeast(1) - onView(withId(R.id.splashScreenBold)).check(matches(onePercentArea)) - onView(withId(R.id.splashScreenNormal)).check(matches(onePercentArea)) + @Test + @UiThread + fun testSplashScreenWithTitlesShouldShowTitles() { + launchActivity().use { scenario -> + scenario.onActivity { + onIdleSync { + onView(withId(R.id.ivSplash)).check(matches(isCompletelyDisplayed())) + + EspressoIdlingResource.increment() + it.setSplashTitles("Example", "Cloud") + EspressoIdlingResource.decrement() + + val onePercentArea = ViewMatchers.isDisplayingAtLeast(1) + onView(withId(R.id.splashScreenBold)).check(matches(onePercentArea)) + onView(withId(R.id.splashScreenNormal)).check(matches(onePercentArea)) + } + } + } } } diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java index 8360db1..5ea2754 100644 --- a/app/src/androidTest/java/com/owncloud/android/AbstractIT.java +++ b/app/src/androidTest/java/com/owncloud/android/AbstractIT.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2018 Tobias Kaminsky - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android; @@ -22,6 +22,8 @@ import android.view.View; import com.facebook.testing.screenshot.Screenshot; import com.facebook.testing.screenshot.internal.TestNameDetector; +import com.nextcloud.android.common.ui.theme.MaterialSchemes; +import com.nextcloud.android.common.ui.theme.MaterialSchemesImpl; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.account.UserAccountManagerImpl; @@ -38,7 +40,6 @@ import com.nextcloud.test.RandomStringGenerator; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.datamodel.UploadsStorageManager; import com.owncloud.android.db.OCUpload; import com.owncloud.android.files.services.NameCollisionPolicy; @@ -54,6 +55,7 @@ import com.owncloud.android.lib.resources.status.OwnCloudVersion; import com.owncloud.android.operations.CreateFolderOperation; import com.owncloud.android.operations.UploadFileOperation; import com.owncloud.android.utils.FileStorageUtils; +import com.owncloud.android.utils.theme.MaterialSchemesProvider; import org.apache.commons.io.FileUtils; import org.junit.After; @@ -182,7 +184,7 @@ public abstract class AbstractIT { String darkModeParameter = arguments.getString("DARKMODE"); if (darkModeParameter != null) { - if (darkModeParameter.equalsIgnoreCase("dark")) { + if ("dark".equalsIgnoreCase(darkModeParameter)) { DARK_MODE = "dark"; AppPreferencesImpl.fromContext(targetContext).setDarkThemeMode(DarkMode.DARK); MainApp.setAppTheme(DarkMode.DARK); @@ -191,7 +193,7 @@ public abstract class AbstractIT { } } - if (DARK_MODE.equalsIgnoreCase("light") && COLOR.equalsIgnoreCase("blue")) { + if ("light".equalsIgnoreCase(DARK_MODE) && "blue".equalsIgnoreCase(COLOR)) { // use already existing names DARK_MODE = ""; COLOR = ""; @@ -254,19 +256,12 @@ public abstract class AbstractIT { file.mkdirs(); return file; } else { - switch (name) { - case "empty.txt": - return createFile("empty.txt", 0); - - case "nonEmpty.txt": - return createFile("nonEmpty.txt", 100); - - case "chunkedFile.txt": - return createFile("chunkedFile.txt", 500000); - - default: - return createFile(name, 0); - } + return switch (name) { + case "empty.txt" -> createFile("empty.txt", 0); + case "nonEmpty.txt" -> createFile("nonEmpty.txt", 100); + case "chunkedFile.txt" -> createFile("chunkedFile.txt", 500000); + default -> createFile(name, 0); + }; } } @@ -301,7 +296,7 @@ public abstract class AbstractIT { InstrumentationRegistry.getInstrumentation().waitForIdleSync(); } - protected void onIdleSync(Runnable recipient) { + public void onIdleSync(Runnable recipient) { InstrumentationRegistry.getInstrumentation().waitForIdle(recipient); } @@ -355,7 +350,7 @@ public abstract class AbstractIT { } } - public OCFile createFolder(String remotePath) { + public void createFolder(String remotePath) { RemoteOperationResult check = new ExistenceCheckRemoteOperation(remotePath, false).execute(client); if (!check.isSuccess()) { @@ -363,8 +358,6 @@ public abstract class AbstractIT { .execute(client) .isSuccess()); } - - return getStorageManager().getFileByDecryptedRemotePath(remotePath.endsWith("/") ? remotePath : remotePath + "/"); } public void uploadFile(File file, String remotePath) { @@ -375,6 +368,11 @@ public abstract class AbstractIT { public void uploadOCUpload(OCUpload ocUpload) { ConnectivityService connectivityServiceMock = new ConnectivityService() { + @Override + public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) { + + } + @Override public boolean isConnected() { return false; @@ -402,11 +400,6 @@ public abstract class AbstractIT { public boolean isPowerSavingEnabled() { return false; } - - @Override - public boolean isPowerSavingExclusionAvailable() { - return false; - } }; UserAccountManager accountManager = UserAccountManagerImpl.fromContext(targetContext); @@ -457,12 +450,18 @@ public abstract class AbstractIT { screenshot(view, ""); } - protected void screenshotViaName(Activity activity, String name) { + public void screenshotViaName(Activity activity, String name) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { Screenshot.snapActivity(activity).setName(name).record(); } } + protected void screenshotViaName(View view, String name) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + Screenshot.snap(view).setName(name).record(); + } + } + protected void screenshot(View view, String prefix) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { Screenshot.snap(view).setName(createName(prefix)).record(); @@ -541,4 +540,38 @@ public abstract class AbstractIT { protected static boolean removeAccount(Account account) { return AccountManager.get(targetContext).removeAccountExplicitly(account); } + + protected MaterialSchemesProvider getMaterialSchemesProvider() { + return new MaterialSchemesProvider() { + @NonNull + @Override + public MaterialSchemes getMaterialSchemesForUser(@NonNull User user) { + return null; + } + + @NonNull + @Override + public MaterialSchemes getMaterialSchemesForCapability(@NonNull OCCapability capability) { + return null; + } + + @NonNull + @Override + public MaterialSchemes getMaterialSchemesForCurrentUser() { + return new MaterialSchemesImpl(R.color.primary, false); + } + + @NonNull + @Override + public MaterialSchemes getDefaultMaterialSchemes() { + return null; + } + + @NonNull + @Override + public MaterialSchemes getMaterialSchemesForPrimaryBackground() { + return null; + } + }; + } } diff --git a/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java b/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java index 78d5acd..fd452db 100644 --- a/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java +++ b/app/src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android; @@ -13,7 +13,6 @@ import android.accounts.OperationCanceledException; import android.content.ActivityNotFoundException; import android.net.Uri; import android.os.Bundle; - import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.account.UserAccountManagerImpl; @@ -174,9 +173,7 @@ public abstract class AbstractOnServerIT extends AbstractIT { Assert.fail("Server not ready!"); } - } catch (IOException e) { - e.printStackTrace(); - } catch (InterruptedException e) { + } catch (IOException | InterruptedException e) { e.printStackTrace(); } } @@ -187,6 +184,11 @@ public abstract class AbstractOnServerIT extends AbstractIT { public void uploadOCUpload(OCUpload ocUpload, int localBehaviour) { ConnectivityService connectivityServiceMock = new ConnectivityService() { + @Override + public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) { + + } + @Override public boolean isConnected() { return false; @@ -214,11 +216,6 @@ public abstract class AbstractOnServerIT extends AbstractIT { public boolean isPowerSavingEnabled() { return false; } - - @Override - public boolean isPowerSavingExclusionAvailable() { - return false; - } }; UserAccountManager accountManager = UserAccountManagerImpl.fromContext(targetContext); diff --git a/app/src/androidTest/java/com/owncloud/android/DownloadIT.java b/app/src/androidTest/java/com/owncloud/android/DownloadIT.java index 7ecd8da..610469f 100644 --- a/app/src/androidTest/java/com/owncloud/android/DownloadIT.java +++ b/app/src/androidTest/java/com/owncloud/android/DownloadIT.java @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android; diff --git a/app/src/androidTest/java/com/owncloud/android/EncryptionIT.kt b/app/src/androidTest/java/com/owncloud/android/EncryptionIT.kt index ed119a2..ec53f46 100644 --- a/app/src/androidTest/java/com/owncloud/android/EncryptionIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/EncryptionIT.kt @@ -1,8 +1,8 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Your Name - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android diff --git a/app/src/androidTest/java/com/owncloud/android/FileIT.java b/app/src/androidTest/java/com/owncloud/android/FileIT.java index ff7ef82..2ba3622 100644 --- a/app/src/androidTest/java/com/owncloud/android/FileIT.java +++ b/app/src/androidTest/java/com/owncloud/android/FileIT.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2018 Tobias Kaminsky - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android; @@ -106,8 +106,8 @@ public class FileIT extends AbstractOnServerIT { assertTrue(new SynchronizeFolderOperation(targetContext, folderPath, user, - System.currentTimeMillis(), - fileDataStorageManager) + fileDataStorageManager, + false) .execute(targetContext) .isSuccess()); diff --git a/app/src/androidTest/java/com/owncloud/android/ScreenshotsIT.java b/app/src/androidTest/java/com/owncloud/android/ScreenshotsIT.java deleted file mode 100644 index f7b95f9..0000000 --- a/app/src/androidTest/java/com/owncloud/android/ScreenshotsIT.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2018 Tobias Kaminsky - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.owncloud.android; - -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.operations.CreateFolderOperation; -import com.owncloud.android.operations.common.SyncOperation; -import com.owncloud.android.ui.activity.FileDisplayActivity; -import com.owncloud.android.ui.activity.SettingsActivity; -import com.owncloud.android.ui.activity.SyncedFoldersActivity; - -import org.junit.Assert; -import org.junit.BeforeClass; -import org.junit.ClassRule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -import androidx.test.core.app.ActivityScenario; -import androidx.test.espresso.action.ViewActions; -import androidx.test.espresso.contrib.DrawerActions; -import androidx.test.espresso.contrib.RecyclerViewActions; -import androidx.test.espresso.matcher.PreferenceMatchers; -import androidx.test.filters.LargeTest; -import tools.fastlane.screengrab.Screengrab; -import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy; -import tools.fastlane.screengrab.locale.LocaleTestRule; - -import static androidx.test.espresso.Espresso.onData; -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.Espresso.pressBack; -import static androidx.test.espresso.action.ViewActions.click; -import static androidx.test.espresso.matcher.ViewMatchers.withId; -import static androidx.test.espresso.matcher.ViewMatchers.withText; -import static org.hamcrest.core.AnyOf.anyOf; -import static org.junit.Assert.assertTrue; - -@LargeTest -@RunWith(JUnit4.class) -public class ScreenshotsIT extends AbstractOnServerIT { - @ClassRule - public static final LocaleTestRule localeTestRule = new LocaleTestRule(); - - @BeforeClass - public static void beforeScreenshot() { - Screengrab.setDefaultScreenshotStrategy(new UiAutomatorScreenshotStrategy()); - } - - @Test - public void gridViewScreenshot() { - ActivityScenario.launch(FileDisplayActivity.class); - - onView(anyOf(withText(R.string.action_switch_grid_view), withId(R.id.switch_grid_view_button))).perform(click()); - - shortSleep(); - - Screengrab.screenshot("01_gridView"); - - onView(anyOf(withText(R.string.action_switch_list_view), withId(R.id.switch_grid_view_button))).perform(click()); - - Assert.assertTrue(true); // if we reach this, everything is ok - } - - @Test - public void listViewScreenshot() { - String path = "/Camera/"; - - // folder does not exist yet - if (getStorageManager().getFileByEncryptedRemotePath(path) == null) { - SyncOperation syncOp = new CreateFolderOperation(path, user, targetContext, getStorageManager()); - RemoteOperationResult result = syncOp.execute(client); - - assertTrue(result.isSuccess()); - } - - ActivityScenario.launch(FileDisplayActivity.class); - - // go into work folder - onView(withId(R.id.list_root)).perform(RecyclerViewActions.actionOnItemAtPosition(0, click())); - - Screengrab.screenshot("02_listView"); - - Assert.assertTrue(true); // if we reach this, everything is ok - } - - @Test - public void drawerScreenshot() { - ActivityScenario.launch(FileDisplayActivity.class); - - onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()); - - Screengrab.screenshot("03_drawer"); - - onView(withId(R.id.drawer_layout)).perform(DrawerActions.close()); - - Assert.assertTrue(true); // if we reach this, everything is ok - } - - @Test - public void multipleAccountsScreenshot() { - ActivityScenario.launch(FileDisplayActivity.class); - - onView(withId(R.id.switch_account_button)).perform(click()); - - Screengrab.screenshot("04_accounts"); - - pressBack(); - - Assert.assertTrue(true); // if we reach this, everything is ok - } - - @Test - public void autoUploadScreenshot() { - ActivityScenario.launch(SyncedFoldersActivity.class); - - Screengrab.screenshot("05_autoUpload"); - - Assert.assertTrue(true); // if we reach this, everything is ok - } - - @Test - public void davdroidScreenshot() { - ActivityScenario.launch(SettingsActivity.class); - - onData(PreferenceMatchers.withTitle(R.string.prefs_category_more)).perform(ViewActions.scrollTo()); - - shortSleep(); - - Screengrab.screenshot("06_davdroid"); - - Assert.assertTrue(true); // if we reach this, everything is ok - } -} diff --git a/app/src/androidTest/java/com/owncloud/android/ScreenshotsIT.kt b/app/src/androidTest/java/com/owncloud/android/ScreenshotsIT.kt new file mode 100644 index 0000000..3edb4f5 --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/ScreenshotsIT.kt @@ -0,0 +1,183 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android + +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.scrollTo +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.contrib.DrawerActions +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.activity.SettingsActivity +import com.owncloud.android.ui.activity.SyncedFoldersActivity +import com.owncloud.android.utils.EspressoIdlingResource +import com.owncloud.android.utils.ScreenshotTest +import org.junit.After +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.BeforeClass +import org.junit.ClassRule +import org.junit.Test +import tools.fastlane.screengrab.Screengrab +import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy +import tools.fastlane.screengrab.locale.LocaleTestRule + +class ScreenshotsIT : AbstractIT() { + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } + + @Test + @UiThread + @ScreenshotTest + fun gridViewScreenshot() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + onView(withId(R.id.switch_grid_view_button)).perform(click()) + EspressoIdlingResource.decrement() + + onView(isRoot()).check(matches(isDisplayed())) + Screengrab.screenshot("01_gridView") + + // Switch back + onView(withId(R.id.switch_grid_view_button)).perform(click()) + + assertTrue(true) + } + } + } + } + + @Test + @UiThread + @ScreenshotTest + fun listViewScreenshot() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + val path = "/Camera/" + OCFile(path).apply { + storageManager.saveFile(this) + } + onView(withId(R.id.list_root)).perform(click()) + EspressoIdlingResource.decrement() + + onView(isRoot()).check(matches(isDisplayed())) + Screengrab.screenshot("02_listView") + assertTrue(true) + } + } + } + } + + @Test + @UiThread + @ScreenshotTest + fun drawerScreenshot() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + onView(withId(R.id.drawer_layout)).perform(DrawerActions.open()) + EspressoIdlingResource.decrement() + + onView(isRoot()).check(matches(isDisplayed())) + Screengrab.screenshot("03_drawer") + + onView(withId(R.id.drawer_layout)).perform(DrawerActions.close()) + assertTrue(true) + } + } + } + } + + @Test + @UiThread + @ScreenshotTest + fun multipleAccountsScreenshot() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + onView(withId(R.id.switch_account_button)).perform(click()) + EspressoIdlingResource.decrement() + + onView(isRoot()).check(matches(isDisplayed())) + Screengrab.screenshot("04_accounts") + + Espresso.pressBack() + assertTrue(true) + } + } + } + } + + @Test + @UiThread + @ScreenshotTest + fun autoUploadScreenshot() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + onView(isRoot()).check(matches(isDisplayed())) + Screengrab.screenshot("05_autoUpload") + assertTrue(true) + } + } + } + } + + @Test + @UiThread + @ScreenshotTest + fun davdroidScreenshot() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + onView(withText(R.string.prefs_category_more)).perform(scrollTo()) + EspressoIdlingResource.decrement() + + onView(isRoot()).check(matches(isDisplayed())) + Screengrab.screenshot("06_davdroid") + assertTrue(true) + } + } + } + } + + companion object { + @ClassRule + @JvmField + val localeTestRule: LocaleTestRule = LocaleTestRule() + + @BeforeClass + @JvmStatic + fun beforeScreenshot() { + Screengrab.setDefaultScreenshotStrategy(UiAutomatorScreenshotStrategy()) + } + } +} diff --git a/app/src/androidTest/java/com/owncloud/android/UploadIT.java b/app/src/androidTest/java/com/owncloud/android/UploadIT.java index cd5d003..8072bb5 100644 --- a/app/src/androidTest/java/com/owncloud/android/UploadIT.java +++ b/app/src/androidTest/java/com/owncloud/android/UploadIT.java @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android; @@ -56,6 +56,11 @@ public class UploadIT extends AbstractOnServerIT { targetContext.getContentResolver()); private ConnectivityService connectivityServiceMock = new ConnectivityService() { + @Override + public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) { + + } + @Override public boolean isConnected() { return false; @@ -77,12 +82,6 @@ public class UploadIT extends AbstractOnServerIT { public boolean isPowerSavingEnabled() { return false; } - - @Override - public boolean isPowerSavingExclusionAvailable() { - return false; - } - @NonNull @Override public BatteryStatus getBattery() { @@ -232,11 +231,6 @@ public class UploadIT extends AbstractOnServerIT { return false; } - @Override - public boolean isPowerSavingExclusionAvailable() { - return false; - } - @NonNull @Override public BatteryStatus getBattery() { @@ -274,6 +268,11 @@ public class UploadIT extends AbstractOnServerIT { @Test public void testUploadOnWifiOnlyButNoWifi() { ConnectivityService connectivityServiceMock = new ConnectivityService() { + @Override + public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) { + + } + @Override public boolean isConnected() { return false; @@ -358,6 +357,11 @@ public class UploadIT extends AbstractOnServerIT { @Test public void testUploadOnWifiOnlyButMeteredWifi() { ConnectivityService connectivityServiceMock = new ConnectivityService() { + @Override + public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) { + + } + @Override public boolean isConnected() { return false; @@ -452,7 +456,7 @@ public class UploadIT extends AbstractOnServerIT { assertEquals(remotePath, ocFile.getRemotePath()); assertEquals(creationTimestamp, ocFile.getCreationTimestamp()); - assertTrue(uploadTimestamp - 10 < ocFile.getUploadTimestamp() || + assertTrue(uploadTimestamp - 10 < ocFile.getUploadTimestamp() && uploadTimestamp + 10 > ocFile.getUploadTimestamp()); } @@ -498,7 +502,7 @@ public class UploadIT extends AbstractOnServerIT { OCFile ocFile = null; for (OCFile f : files) { - if (f.getFileName().equals("metadata.jpg")) { + if ("metadata.jpg".equals(f.getFileName())) { ocFile = f; break; } diff --git a/app/src/androidTest/java/com/owncloud/android/authentication/AuthenticatorActivityIT.kt b/app/src/androidTest/java/com/owncloud/android/authentication/AuthenticatorActivityIT.kt index 996ea51..0ed8b7f 100644 --- a/app/src/androidTest/java/com/owncloud/android/authentication/AuthenticatorActivityIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/authentication/AuthenticatorActivityIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.authentication diff --git a/app/src/androidTest/java/com/owncloud/android/authentication/PassCodeManagerIT.kt b/app/src/androidTest/java/com/owncloud/android/authentication/PassCodeManagerIT.kt index d8ab5e2..7d616c4 100644 --- a/app/src/androidTest/java/com/owncloud/android/authentication/PassCodeManagerIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/authentication/PassCodeManagerIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Álvaro Brey * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.authentication diff --git a/app/src/androidTest/java/com/owncloud/android/datamodel/ArbitraryDataProviderIT.kt b/app/src/androidTest/java/com/owncloud/android/datamodel/ArbitraryDataProviderIT.kt index 2de4267..6188d5e 100644 --- a/app/src/androidTest/java/com/owncloud/android/datamodel/ArbitraryDataProviderIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/datamodel/ArbitraryDataProviderIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel diff --git a/app/src/androidTest/java/com/owncloud/android/datamodel/ContentResolverHelperIT.kt b/app/src/androidTest/java/com/owncloud/android/datamodel/ContentResolverHelperIT.kt index d2c9e0b..1a5624f 100644 --- a/app/src/androidTest/java/com/owncloud/android/datamodel/ContentResolverHelperIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/datamodel/ContentResolverHelperIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2021 Álvaro Brey * SPDX-FileCopyrightText: 2021 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel @@ -43,7 +43,6 @@ class ContentResolverHelperIT { } @Test - @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) fun contentResolver_onAndroid26_usesNewAPI() { ContentResolverHelper .queryResolver(resolver, URI, PROJECTION, SELECTION, null, SORT_COLUMN, SORT_DIRECTION, LIMIT) diff --git a/app/src/androidTest/java/com/owncloud/android/datamodel/Credentials.kt b/app/src/androidTest/java/com/owncloud/android/datamodel/Credentials.kt new file mode 100644 index 0000000..5bcaf7c --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/datamodel/Credentials.kt @@ -0,0 +1,10 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.datamodel + +data class Credentials(val publicKey: String, val certificate: String) diff --git a/app/src/androidTest/java/com/owncloud/android/datamodel/FileDataStorageManagerContentProviderClientIT.java b/app/src/androidTest/java/com/owncloud/android/datamodel/FileDataStorageManagerContentProviderClientIT.java index 3e435d3..76c576c 100644 --- a/app/src/androidTest/java/com/owncloud/android/datamodel/FileDataStorageManagerContentProviderClientIT.java +++ b/app/src/androidTest/java/com/owncloud/android/datamodel/FileDataStorageManagerContentProviderClientIT.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel; diff --git a/app/src/androidTest/java/com/owncloud/android/datamodel/FileDataStorageManagerContentResolverIT.kt b/app/src/androidTest/java/com/owncloud/android/datamodel/FileDataStorageManagerContentResolverIT.kt index 3bb41ef..6c800da 100644 --- a/app/src/androidTest/java/com/owncloud/android/datamodel/FileDataStorageManagerContentResolverIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/datamodel/FileDataStorageManagerContentResolverIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel diff --git a/app/src/androidTest/java/com/owncloud/android/datamodel/FileDataStorageManagerIT.java b/app/src/androidTest/java/com/owncloud/android/datamodel/FileDataStorageManagerIT.java index f3bae17..34f8061 100644 --- a/app/src/androidTest/java/com/owncloud/android/datamodel/FileDataStorageManagerIT.java +++ b/app/src/androidTest/java/com/owncloud/android/datamodel/FileDataStorageManagerIT.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel; diff --git a/app/src/androidTest/java/com/owncloud/android/datamodel/OCCapabilityIT.kt b/app/src/androidTest/java/com/owncloud/android/datamodel/OCCapabilityIT.kt index baf2c61..121d630 100644 --- a/app/src/androidTest/java/com/owncloud/android/datamodel/OCCapabilityIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/datamodel/OCCapabilityIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel diff --git a/app/src/androidTest/java/com/owncloud/android/datamodel/OCFileIconTests.kt b/app/src/androidTest/java/com/owncloud/android/datamodel/OCFileIconTests.kt index 0c130f9..59167c6 100644 --- a/app/src/androidTest/java/com/owncloud/android/datamodel/OCFileIconTests.kt +++ b/app/src/androidTest/java/com/owncloud/android/datamodel/OCFileIconTests.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel diff --git a/app/src/androidTest/java/com/owncloud/android/datamodel/OCFileUnitTest.java b/app/src/androidTest/java/com/owncloud/android/datamodel/OCFileUnitTest.java index 1c73a50..54efa0c 100644 --- a/app/src/androidTest/java/com/owncloud/android/datamodel/OCFileUnitTest.java +++ b/app/src/androidTest/java/com/owncloud/android/datamodel/OCFileUnitTest.java @@ -36,6 +36,7 @@ public class OCFileUnitTest { private static final String STORAGE_PATH = "/mnt/sd/localpath/to/a/file.txt"; private static final String MIME_TYPE = "text/plain"; private static final long FILE_LENGTH = 9876543210L; + private static final long UPLOADED_TIMESTAMP = 8765431109L; private static final long CREATION_TIMESTAMP = 8765432109L; private static final long MODIFICATION_TIMESTAMP = 7654321098L; private static final long MODIFICATION_TIMESTAMP_AT_LAST_SYNC_FOR_DATA = 6543210987L; @@ -63,6 +64,7 @@ public class OCFileUnitTest { mFile.setStoragePath(STORAGE_PATH); mFile.setMimeType(MIME_TYPE); mFile.setFileLength(FILE_LENGTH); + mFile.setUploadTimestamp(UPLOADED_TIMESTAMP); mFile.setCreationTimestamp(CREATION_TIMESTAMP); mFile.setModificationTimestamp(MODIFICATION_TIMESTAMP); mFile.setModificationTimestampAtLastSyncForData(MODIFICATION_TIMESTAMP_AT_LAST_SYNC_FOR_DATA); @@ -93,6 +95,7 @@ public class OCFileUnitTest { assertThat(fileReadFromParcel.getStoragePath(), is(STORAGE_PATH)); assertThat(fileReadFromParcel.getMimeType(), is(MIME_TYPE)); assertThat(fileReadFromParcel.getFileLength(), is(FILE_LENGTH)); + assertThat(fileReadFromParcel.getUploadTimestamp(), is(UPLOADED_TIMESTAMP)); assertThat(fileReadFromParcel.getCreationTimestamp(), is(CREATION_TIMESTAMP)); assertThat(fileReadFromParcel.getModificationTimestamp(), is(MODIFICATION_TIMESTAMP)); assertThat( diff --git a/app/src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.java b/app/src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.java index 2bf5702..9c255ef 100644 --- a/app/src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.java +++ b/app/src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.java @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2017 JARP * SPDX-FileCopyrightText: 2021 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel; @@ -18,6 +18,7 @@ import com.nextcloud.client.account.CurrentAccountProvider; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.account.UserAccountManagerImpl; +import com.nextcloud.client.database.entity.UploadEntityKt; import com.nextcloud.test.RandomStringGenerator; import com.owncloud.android.AbstractIT; import com.owncloud.android.MainApp; @@ -108,7 +109,7 @@ public class UploadStorageManagerTest extends AbstractIT { OCUpload upload = createUpload(account); uploads.add(upload); - uploadsStorageManager.storeUpload(upload); + uploadsStorageManager.uploadDao.insertOrReplace(UploadEntityKt.toUploadEntity(upload)); } OCUpload[] storedUploads = uploadsStorageManager.getAllStoredUploads(); @@ -151,17 +152,14 @@ public class UploadStorageManagerTest extends AbstractIT { account.name); corruptUpload.setLocalPath(null); - - uploadsStorageManager.storeUpload(corruptUpload); - + uploadsStorageManager.uploadDao.insertOrReplace(UploadEntityKt.toUploadEntity(corruptUpload)); uploadsStorageManager.getAllStoredUploads(); } @Test public void getById() { OCUpload upload = createUpload(account); - long id = uploadsStorageManager.storeUpload(upload); - + long id = uploadsStorageManager.uploadDao.insertOrReplace(UploadEntityKt.toUploadEntity(upload)); OCUpload newUpload = uploadsStorageManager.getUploadById(id); assertNotNull(newUpload); @@ -178,7 +176,7 @@ public class UploadStorageManagerTest extends AbstractIT { private void insertUploads(Account account, int rowsToInsert) { for (int i = 0; i < rowsToInsert; i++) { - uploadsStorageManager.storeUpload(createUpload(account)); + uploadsStorageManager.uploadDao.insertOrReplace(UploadEntityKt.toUploadEntity(createUpload(account))); } } diff --git a/app/src/androidTest/java/com/owncloud/android/extensions/AbstractITExtensions.kt b/app/src/androidTest/java/com/owncloud/android/extensions/AbstractITExtensions.kt new file mode 100644 index 0000000..f6b6623 --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/extensions/AbstractITExtensions.kt @@ -0,0 +1,39 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.extensions + +import android.app.Activity +import android.content.Intent +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import com.owncloud.android.AbstractIT +import com.owncloud.android.utils.EspressoIdlingResource +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed + +inline fun AbstractIT.launchAndCapture( + testClassName: String, + actionName: String, + intent: Intent? = null, + crossinline before: (T) -> Unit +) { + launchActivity(intent).use { scenario -> + scenario.onActivity { activity -> + onIdleSync { + EspressoIdlingResource.increment() + before(activity) + EspressoIdlingResource.decrement() + + val screenshotName = createName(testClassName + "_" + actionName, "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(activity, screenshotName) + } + } + } +} diff --git a/app/src/androidTest/java/com/owncloud/android/files/FileMenuFilterIT.kt b/app/src/androidTest/java/com/owncloud/android/files/FileMenuFilterIT.kt index 7a7ea10..5de86fc 100644 --- a/app/src/androidTest/java/com/owncloud/android/files/FileMenuFilterIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/files/FileMenuFilterIT.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2022 Álvaro Brey Vilas + * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later */ package com.owncloud.android.files @@ -331,10 +331,7 @@ class FileMenuFilterIT : AbstractIT() { } } - private data class ExpectedLockVisibilities( - val lockFile: Boolean, - val unlockFile: Boolean - ) + private data class ExpectedLockVisibilities(val lockFile: Boolean, val unlockFile: Boolean) private fun configureCapability(capability: OCCapability) { every { mockStorageManager.getCapability(any()) } returns capability diff --git a/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt b/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt index 13b04c0..e9f8444 100644 --- a/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.files.services @@ -34,9 +34,9 @@ abstract class FileUploaderIT : AbstractOnServerIT() { private var uploadsStorageManager: UploadsStorageManager? = null private val connectivityServiceMock: ConnectivityService = object : ConnectivityService { - override fun isConnected(): Boolean { - return false - } + override fun isNetworkAndServerAvailable(callback: ConnectivityService.GenericCallback) = Unit + + override fun isConnected(): Boolean = false override fun isInternetWalled(): Boolean = false override fun getConnectivity(): Connectivity = Connectivity.CONNECTED_WIFI @@ -46,9 +46,6 @@ abstract class FileUploaderIT : AbstractOnServerIT() { override val isPowerSavingEnabled: Boolean get() = false - override val isPowerSavingExclusionAvailable: Boolean - get() = false - override val battery: BatteryStatus get() = BatteryStatus() } @@ -327,7 +324,7 @@ abstract class FileUploaderIT : AbstractOnServerIT() { user, null, ocUpload2, - NameCollisionPolicy.CANCEL, + NameCollisionPolicy.SKIP, FileUploadWorker.LOCAL_BEHAVIOUR_COPY, targetContext, false, @@ -376,7 +373,7 @@ abstract class FileUploaderIT : AbstractOnServerIT() { user, arrayOf(ocFile2), FileUploadWorker.LOCAL_BEHAVIOUR_COPY, - NameCollisionPolicy.CANCEL + NameCollisionPolicy.SKIP ) shortSleep() @@ -403,7 +400,7 @@ abstract class FileUploaderIT : AbstractOnServerIT() { user, null, ocUpload, - NameCollisionPolicy.CANCEL, + NameCollisionPolicy.SKIP, FileUploadWorker.LOCAL_BEHAVIOUR_COPY, targetContext, false, @@ -429,7 +426,7 @@ abstract class FileUploaderIT : AbstractOnServerIT() { user, null, ocUpload2, - NameCollisionPolicy.CANCEL, + NameCollisionPolicy.SKIP, FileUploadWorker.LOCAL_BEHAVIOUR_COPY, targetContext, false, @@ -480,7 +477,7 @@ abstract class FileUploaderIT : AbstractOnServerIT() { user, arrayOf(ocFile2), FileUploadWorker.LOCAL_BEHAVIOUR_COPY, - NameCollisionPolicy.CANCEL + NameCollisionPolicy.SKIP ) shortSleep() diff --git a/app/src/androidTest/java/com/owncloud/android/files/services/LegacyFileUploaderIT.kt b/app/src/androidTest/java/com/owncloud/android/files/services/LegacyFileUploaderIT.kt deleted file mode 100644 index e8cc664..0000000 --- a/app/src/androidTest/java/com/owncloud/android/files/services/LegacyFileUploaderIT.kt +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2022 Tobias Kaminsky - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.owncloud.android.files.services - -class LegacyFileUploaderIT : FileUploaderIT() diff --git a/app/src/androidTest/java/com/owncloud/android/operations/GetSharesForFileOperationIT.kt b/app/src/androidTest/java/com/owncloud/android/operations/GetSharesForFileOperationIT.kt index 5685531..1d268ce 100644 --- a/app/src/androidTest/java/com/owncloud/android/operations/GetSharesForFileOperationIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/operations/GetSharesForFileOperationIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2021 Tobias Kaminsky * SPDX-FileCopyrightText: 2021 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.operations diff --git a/app/src/androidTest/java/com/owncloud/android/operations/RemoveFileOperationIT.java b/app/src/androidTest/java/com/owncloud/android/operations/RemoveFileOperationIT.java index c2adc36..33f0910 100644 --- a/app/src/androidTest/java/com/owncloud/android/operations/RemoveFileOperationIT.java +++ b/app/src/androidTest/java/com/owncloud/android/operations/RemoveFileOperationIT.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.operations; diff --git a/app/src/androidTest/java/com/owncloud/android/providers/DocumentsProviderUtils.kt b/app/src/androidTest/java/com/owncloud/android/providers/DocumentsProviderUtils.kt index 0a130da..cd5f695 100644 --- a/app/src/androidTest/java/com/owncloud/android/providers/DocumentsProviderUtils.kt +++ b/app/src/androidTest/java/com/owncloud/android/providers/DocumentsProviderUtils.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Torsten Grote - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.providers @@ -177,31 +177,30 @@ object DocumentsProviderUtils { */ @Suppress("EXPERIMENTAL_API_USAGE") @VisibleForTesting - internal suspend fun getLoadedCursor(timeout: Long = 15_000, query: () -> Cursor?) = - withTimeout(timeout) { - suspendCancellableCoroutine { cont -> - val cursor = query() ?: throw IOException("Initial query returned no results") - cont.invokeOnCancellation { cursor.close() } - val loading = cursor.extras.getBoolean(EXTRA_LOADING, false) - if (loading) { - Log_OC.e("TEST", "Cursor was loading, wait for update...") - cursor.registerContentObserver( - object : ContentObserver(null) { - override fun onChange(selfChange: Boolean, uri: Uri?) { - cursor.close() - val newCursor = query() - if (newCursor == null) { - cont.cancel(IOException("Re-query returned no results")) - } else { - cont.resume(newCursor) - } + internal suspend fun getLoadedCursor(timeout: Long = 15_000, query: () -> Cursor?) = withTimeout(timeout) { + suspendCancellableCoroutine { cont -> + val cursor = query() ?: throw IOException("Initial query returned no results") + cont.invokeOnCancellation { cursor.close() } + val loading = cursor.extras?.getBoolean(EXTRA_LOADING, false) ?: false + if (loading) { + Log_OC.e("TEST", "Cursor was loading, wait for update...") + cursor.registerContentObserver( + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + cursor.close() + val newCursor = query() + if (newCursor == null) { + cont.cancel(IOException("Re-query returned no results")) + } else { + cont.resume(newCursor) } } - ) - } else { - // not loading, return cursor right away - cont.resume(cursor) - } + } + ) + } else { + // not loading, return cursor right away + cont.resume(cursor) } } + } } diff --git a/app/src/androidTest/java/com/owncloud/android/providers/DocumentsStorageProviderIT.kt b/app/src/androidTest/java/com/owncloud/android/providers/DocumentsStorageProviderIT.kt index 7510497..3bdd0e0 100644 --- a/app/src/androidTest/java/com/owncloud/android/providers/DocumentsStorageProviderIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/providers/DocumentsStorageProviderIT.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Torsten Grote - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.providers @@ -53,6 +53,14 @@ class DocumentsStorageProviderIT : AbstractOnServerIT() { // DocumentsProvider#onCreate() is called when the application is started // which is *after* AbstractOnServerIT adds the accounts (when the app is freshly installed). // So we need to query our roots here to ensure that the internal storage map is initialized. + storageManager.run { + val updatedRootPath = getFileByEncryptedRemotePath(ROOT_PATH).apply { + permissions = "RSMCKGWDNV" + } + + saveFile(updatedRootPath) + } + contentResolver.query(DocumentsContract.buildRootsUri(authority), null, null, null) assertTrue("Storage root does not exist", rootDir.exists()) assertTrue(rootDir.isDirectory) diff --git a/app/src/androidTest/java/com/owncloud/android/providers/FileContentProviderVerificationIT.kt b/app/src/androidTest/java/com/owncloud/android/providers/FileContentProviderVerificationIT.kt index 3dbf7fa..573f320 100644 --- a/app/src/androidTest/java/com/owncloud/android/providers/FileContentProviderVerificationIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/providers/FileContentProviderVerificationIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2021 Tobias Kaminsky * SPDX-FileCopyrightText: 2021 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.providers diff --git a/app/src/androidTest/java/com/owncloud/android/providers/UsersAndGroupsSearchProviderIT.kt b/app/src/androidTest/java/com/owncloud/android/providers/UsersAndGroupsSearchProviderIT.kt index 1be0e70..0e660d2 100644 --- a/app/src/androidTest/java/com/owncloud/android/providers/UsersAndGroupsSearchProviderIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/providers/UsersAndGroupsSearchProviderIT.kt @@ -1,32 +1,32 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.providers -import androidx.test.espresso.intent.rule.IntentsTestRule +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot import com.nextcloud.test.TestActivity import com.owncloud.android.AbstractOnServerIT -import org.junit.Rule import org.junit.Test class UsersAndGroupsSearchProviderIT : AbstractOnServerIT() { - @get:Rule - val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false) - @Test + @UiThread fun searchUser() { - val activity = testActivityRule.launchActivity(null) - - shortSleep() - - activity.runOnUiThread { - // fragment.search("Admin") + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + onView(isRoot()).check(matches(isDisplayed())) + } + } } - - longSleep() } } 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 42924cd..9601da9 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/LoginIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/LoginIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui @@ -45,14 +45,13 @@ class LoginIT : AbstractIT() { ActivityScenario.launch(AuthenticatorActivity::class.java) } - @Test - @Throws(InterruptedException::class) - @Suppress("MagicNumber", "SwallowedException") - /** * 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. */ + @Test + @Throws(InterruptedException::class) + @Suppress("MagicNumber", "SwallowedException") @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) fun login() { val arguments = InstrumentationRegistry.getArguments() @@ -71,7 +70,7 @@ class LoginIT : AbstractIT() { Web.onWebView() .withElement(DriverAtoms.findElement(Locator.XPATH, "//form[@id='login-form']/input[@type='submit']")) .perform(DriverAtoms.webClick()) - } catch (e: RuntimeException) { + } catch (_: RuntimeException) { // NC < 25 Web.onWebView() .withElement(DriverAtoms.findElement(Locator.XPATH, "//p[@id='redirect-link']/a")) @@ -94,7 +93,7 @@ class LoginIT : AbstractIT() { Web.onWebView() .withElement(DriverAtoms.findElement(Locator.XPATH, "//button[@type='submit']")) .perform(DriverAtoms.webClick()) - } catch (e: RuntimeException) { + } catch (_: RuntimeException) { // NC < 25 Web.onWebView() .withElement(DriverAtoms.findElement(Locator.XPATH, "//input[@type='submit']")) diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/ConflictsResolveActivityIT.java b/app/src/androidTest/java/com/owncloud/android/ui/activity/ConflictsResolveActivityIT.java deleted file mode 100644 index 4ac55e6..0000000 --- a/app/src/androidTest/java/com/owncloud/android/ui/activity/ConflictsResolveActivityIT.java +++ /dev/null @@ -1,318 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.owncloud.android.ui.activity; - -import android.content.Intent; - -import com.nextcloud.client.account.UserAccountManagerImpl; -import com.owncloud.android.AbstractIT; -import com.owncloud.android.R; -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.db.OCUpload; -import com.owncloud.android.ui.dialog.ConflictsResolveDialog; -import com.owncloud.android.utils.FileStorageUtils; -import com.owncloud.android.utils.ScreenshotTest; - -import org.junit.After; -import org.junit.Rule; -import org.junit.Test; - -import java.util.Objects; - -import androidx.fragment.app.DialogFragment; -import androidx.test.espresso.intent.rule.IntentsTestRule; - -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.action.ViewActions.click; -import static androidx.test.espresso.matcher.ViewMatchers.withId; -import static androidx.test.espresso.matcher.ViewMatchers.withText; -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; -import static junit.framework.TestCase.assertTrue; -import static org.junit.Assert.assertEquals; - -public class ConflictsResolveActivityIT extends AbstractIT { - @Rule public IntentsTestRule activityRule = - new IntentsTestRule<>(ConflictsResolveActivity.class, true, false); - private boolean returnCode; - - @Test - @ScreenshotTest - public void screenshotTextFiles() { - OCFile newFile = new OCFile("/newFile.txt"); - newFile.setFileLength(56000); - newFile.setModificationTimestamp(1522019340); - newFile.setStoragePath(FileStorageUtils.getSavePath(user.getAccountName()) + "/nonEmpty.txt"); - - OCFile existingFile = new OCFile("/newFile.txt"); - existingFile.setFileLength(1024000); - existingFile.setModificationTimestamp(1582019340); - - FileDataStorageManager storageManager = new FileDataStorageManager(user, targetContext.getContentResolver()); - storageManager.saveNewFile(existingFile); - - Intent intent = new Intent(targetContext, ConflictsResolveActivity.class); - intent.putExtra(ConflictsResolveActivity.EXTRA_FILE, newFile); - intent.putExtra(ConflictsResolveActivity.EXTRA_EXISTING_FILE, existingFile); - - ConflictsResolveActivity sut = activityRule.launchActivity(intent); - - ConflictsResolveDialog dialog = ConflictsResolveDialog.newInstance(existingFile, - newFile, - UserAccountManagerImpl - .fromContext(targetContext) - .getUser() - ); - dialog.showDialog(sut); - - getInstrumentation().waitForIdleSync(); - - shortSleep(); - shortSleep(); - shortSleep(); - shortSleep(); - - screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView()); - } - -// @Test - // @ScreenshotTest // todo run without real server -// public void screenshotImages() throws IOException { -// FileDataStorageManager storageManager = new FileDataStorageManager(user, -// targetContext.getContentResolver()); -// -// OCFile newFile = new OCFile("/newFile.txt"); -// newFile.setFileLength(56000); -// newFile.setModificationTimestamp(1522019340); -// newFile.setStoragePath(FileStorageUtils.getSavePath(user.getAccountName()) + "/nonEmpty.txt"); -// -// File image = getFile("image.jpg"); -// -// assertTrue(new UploadFileRemoteOperation(image.getAbsolutePath(), -// "/image.jpg", -// "image/jpg", -// "10000000").execute(client).isSuccess()); -// -// assertTrue(new RefreshFolderOperation(storageManager.getFileByPath("/"), -// System.currentTimeMillis(), -// false, -// true, -// storageManager, -// user.toPlatformAccount(), -// targetContext -// ).execute(client).isSuccess()); -// -// OCFile existingFile = storageManager.getFileByPath("/image.jpg"); -// -// Intent intent = new Intent(targetContext, ConflictsResolveActivity.class); -// intent.putExtra(ConflictsResolveActivity.EXTRA_FILE, newFile); -// intent.putExtra(ConflictsResolveActivity.EXTRA_EXISTING_FILE, existingFile); -// -// ConflictsResolveActivity sut = activityRule.launchActivity(intent); -// -// ConflictsResolveDialog.OnConflictDecisionMadeListener listener = decision -> { -// -// }; -// -// ConflictsResolveDialog dialog = ConflictsResolveDialog.newInstance(existingFile, -// newFile, -// UserAccountManagerImpl -// .fromContext(targetContext) -// .getUser() -// ); -// dialog.showDialog(sut); -// dialog.listener = listener; -// -// getInstrumentation().waitForIdleSync(); -// shortSleep(); -// -// screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView()); -// } - - @Test - public void cancel() { - returnCode = false; - - OCUpload newUpload = new OCUpload(FileStorageUtils.getSavePath(user.getAccountName()) + "/nonEmpty.txt", - "/newFile.txt", - user.getAccountName()); - - OCFile existingFile = new OCFile("/newFile.txt"); - existingFile.setFileLength(1024000); - existingFile.setModificationTimestamp(1582019340); - - OCFile newFile = new OCFile("/newFile.txt"); - newFile.setFileLength(56000); - newFile.setModificationTimestamp(1522019340); - newFile.setStoragePath(FileStorageUtils.getSavePath(user.getAccountName()) + "/nonEmpty.txt"); - - FileDataStorageManager storageManager = new FileDataStorageManager(user, targetContext.getContentResolver()); - storageManager.saveNewFile(existingFile); - - Intent intent = new Intent(targetContext, ConflictsResolveActivity.class); - intent.putExtra(ConflictsResolveActivity.EXTRA_FILE, newFile); - intent.putExtra(ConflictsResolveActivity.EXTRA_EXISTING_FILE, existingFile); - intent.putExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD_ID, newUpload.getUploadId()); - - ConflictsResolveActivity sut = activityRule.launchActivity(intent); - - sut.listener = decision -> { - assertEquals(decision, ConflictsResolveDialog.Decision.CANCEL); - returnCode = true; - }; - - getInstrumentation().waitForIdleSync(); - shortSleep(); - - onView(withText("Cancel")).perform(click()); - - assertTrue(returnCode); - } - - @Test - @ScreenshotTest - public void keepExisting() { - returnCode = false; - - OCUpload newUpload = new OCUpload(FileStorageUtils.getSavePath(user.getAccountName()) + "/nonEmpty.txt", - "/newFile.txt", - user.getAccountName()); - - OCFile existingFile = new OCFile("/newFile.txt"); - existingFile.setFileLength(1024000); - existingFile.setModificationTimestamp(1582019340); - - OCFile newFile = new OCFile("/newFile.txt"); - newFile.setFileLength(56000); - newFile.setModificationTimestamp(1522019340); - newFile.setStoragePath(FileStorageUtils.getSavePath(user.getAccountName()) + "/nonEmpty.txt"); - - FileDataStorageManager storageManager = new FileDataStorageManager(user, targetContext.getContentResolver()); - storageManager.saveNewFile(existingFile); - - Intent intent = new Intent(targetContext, ConflictsResolveActivity.class); - intent.putExtra(ConflictsResolveActivity.EXTRA_FILE, newFile); - intent.putExtra(ConflictsResolveActivity.EXTRA_EXISTING_FILE, existingFile); - intent.putExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD_ID, newUpload.getUploadId()); - - ConflictsResolveActivity sut = activityRule.launchActivity(intent); - - sut.listener = decision -> { - assertEquals(decision, ConflictsResolveDialog.Decision.KEEP_SERVER); - returnCode = true; - }; - - getInstrumentation().waitForIdleSync(); - - onView(withId(R.id.existing_checkbox)).perform(click()); - - DialogFragment dialog = (DialogFragment) sut.getSupportFragmentManager().findFragmentByTag("conflictDialog"); - screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView()); - - onView(withText("OK")).perform(click()); - - assertTrue(returnCode); - } - - @Test - @ScreenshotTest - public void keepNew() { - returnCode = false; - - OCUpload newUpload = new OCUpload(FileStorageUtils.getSavePath(user.getAccountName()) + "/nonEmpty.txt", - "/newFile.txt", - user.getAccountName()); - - OCFile existingFile = new OCFile("/newFile.txt"); - existingFile.setFileLength(1024000); - existingFile.setModificationTimestamp(1582019340); - existingFile.setRemoteId("00000123abc"); - - OCFile newFile = new OCFile("/newFile.txt"); - newFile.setFileLength(56000); - newFile.setModificationTimestamp(1522019340); - newFile.setStoragePath(FileStorageUtils.getSavePath(user.getAccountName()) + "/nonEmpty.txt"); - - FileDataStorageManager storageManager = new FileDataStorageManager(user, targetContext.getContentResolver()); - storageManager.saveNewFile(existingFile); - - Intent intent = new Intent(targetContext, ConflictsResolveActivity.class); - intent.putExtra(ConflictsResolveActivity.EXTRA_FILE, newFile); - intent.putExtra(ConflictsResolveActivity.EXTRA_EXISTING_FILE, existingFile); - intent.putExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD_ID, newUpload.getUploadId()); - - ConflictsResolveActivity sut = activityRule.launchActivity(intent); - - sut.listener = decision -> { - assertEquals(decision, ConflictsResolveDialog.Decision.KEEP_LOCAL); - returnCode = true; - }; - - getInstrumentation().waitForIdleSync(); - - onView(withId(R.id.new_checkbox)).perform(click()); - - DialogFragment dialog = (DialogFragment) sut.getSupportFragmentManager().findFragmentByTag("conflictDialog"); - screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView()); - - onView(withText("OK")).perform(click()); - - assertTrue(returnCode); - } - - @Test - @ScreenshotTest - public void keepBoth() { - returnCode = false; - - OCUpload newUpload = new OCUpload(FileStorageUtils.getSavePath(user.getAccountName()) + "/nonEmpty.txt", - "/newFile.txt", - user.getAccountName()); - - OCFile existingFile = new OCFile("/newFile.txt"); - existingFile.setFileLength(1024000); - existingFile.setModificationTimestamp(1582019340); - - OCFile newFile = new OCFile("/newFile.txt"); - newFile.setFileLength(56000); - newFile.setModificationTimestamp(1522019340); - newFile.setStoragePath(FileStorageUtils.getSavePath(user.getAccountName()) + "/nonEmpty.txt"); - - FileDataStorageManager storageManager = new FileDataStorageManager(user, targetContext.getContentResolver()); - storageManager.saveNewFile(existingFile); - - Intent intent = new Intent(targetContext, ConflictsResolveActivity.class); - intent.putExtra(ConflictsResolveActivity.EXTRA_FILE, newFile); - intent.putExtra(ConflictsResolveActivity.EXTRA_EXISTING_FILE, existingFile); - intent.putExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD_ID, newUpload.getUploadId()); - - ConflictsResolveActivity sut = activityRule.launchActivity(intent); - - sut.listener = decision -> { - assertEquals(decision, ConflictsResolveDialog.Decision.KEEP_BOTH); - returnCode = true; - }; - - getInstrumentation().waitForIdleSync(); - - onView(withId(R.id.existing_checkbox)).perform(click()); - onView(withId(R.id.new_checkbox)).perform(click()); - - DialogFragment dialog = (DialogFragment) sut.getSupportFragmentManager().findFragmentByTag("conflictDialog"); - screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView()); - - onView(withText("OK")).perform(click()); - - assertTrue(returnCode); - } - - @After - public void after() { - getStorageManager().deleteAllFiles(); - } -} diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/ConflictsResolveActivityIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/activity/ConflictsResolveActivityIT.kt new file mode 100644 index 0000000..a1c4845 --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/ui/activity/ConflictsResolveActivityIT.kt @@ -0,0 +1,326 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.owncloud.android.ui.activity + +import android.content.Intent +import androidx.annotation.UiThread +import androidx.fragment.app.DialogFragment +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import com.nextcloud.client.account.UserAccountManagerImpl +import com.nextcloud.utils.extensions.getDecryptedPath +import com.owncloud.android.AbstractIT +import com.owncloud.android.R +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.db.OCUpload +import com.owncloud.android.ui.dialog.ConflictsResolveDialog.Companion.newInstance +import com.owncloud.android.ui.dialog.ConflictsResolveDialog.Decision +import com.owncloud.android.ui.dialog.ConflictsResolveDialog.OnConflictDecisionMadeListener +import com.owncloud.android.utils.EspressoIdlingResource +import com.owncloud.android.utils.FileStorageUtils +import com.owncloud.android.utils.ScreenshotTest +import junit.framework.TestCase +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class ConflictsResolveActivityIT : AbstractIT() { + private val testClassName = "com.owncloud.android.ui.activity.ConflictsResolveActivityIT" + private var returnCode = false + + @Test + @UiThread + @ScreenshotTest + fun screenshotTextFiles() { + val newFile = OCFile("/newFile.txt").apply { + remoteId = "0001" + fileLength = 56000 + modificationTimestamp = 1522019340 + setStoragePath(FileStorageUtils.getSavePath(user.accountName) + "/nonEmpty.txt") + } + + val existingFile = OCFile("/newFile.txt").apply { + remoteId = "0002" + fileLength = 1024000 + modificationTimestamp = 1582019340 + } + + val storageManager = FileDataStorageManager(user, targetContext.contentResolver) + storageManager.saveNewFile(existingFile) + + val intent = Intent(targetContext, ConflictsResolveActivity::class.java).apply { + putExtra(FileActivity.EXTRA_FILE, newFile) + putExtra(ConflictsResolveActivity.EXTRA_EXISTING_FILE, existingFile) + } + + launchActivity(intent).use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + + val dialog = newInstance( + storageManager.getDecryptedPath(existingFile), + targetContext, + newFile, + existingFile, + UserAccountManagerImpl + .fromContext(targetContext) + .getUser() + ) + dialog.showDialog(sut) + + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "screenshotTextFiles", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(dialog.requireDialog().window?.decorView, screenShotName) + } + } + } + } + + @Test + @UiThread + fun cancel() { + val newUpload = OCUpload( + FileStorageUtils.getSavePath(user.accountName) + "/nonEmpty.txt", + "/newFile.txt", + user.accountName + ) + + val existingFile = OCFile("/newFile.txt").apply { + fileLength = 1024000 + modificationTimestamp = 1582019340 + } + + val newFile = OCFile("/newFile.txt").apply { + fileLength = 56000 + modificationTimestamp = 1522019340 + setStoragePath(FileStorageUtils.getSavePath(user.accountName) + "/nonEmpty.txt") + } + + EspressoIdlingResource.increment() + FileDataStorageManager(user, targetContext.contentResolver).run { + saveNewFile(existingFile) + } + EspressoIdlingResource.decrement() + + val intent = Intent(targetContext, ConflictsResolveActivity::class.java).apply { + putExtra(FileActivity.EXTRA_FILE, newFile) + putExtra(ConflictsResolveActivity.EXTRA_EXISTING_FILE, existingFile) + putExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD_ID, newUpload.uploadId) + } + + launchActivity(intent).use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + returnCode = false + sut.listener = OnConflictDecisionMadeListener { decision: Decision? -> + assertEquals(decision, Decision.CANCEL) + returnCode = true + } + EspressoIdlingResource.decrement() + + onView(ViewMatchers.withText("Cancel")).perform(ViewActions.click()) + TestCase.assertTrue(returnCode) + } + } + } + } + + @Test + @UiThread + @ScreenshotTest + fun keepExisting() { + returnCode = false + + val newUpload = OCUpload( + FileStorageUtils.getSavePath(user.accountName) + "/nonEmpty.txt", + "/newFile.txt", + user.accountName + ) + + val existingFile = OCFile("/newFile.txt").apply { + remoteId = "0001" + fileLength = 1024000 + modificationTimestamp = 1582019340 + } + + val newFile = OCFile("/newFile.txt").apply { + fileLength = 56000 + remoteId = "0002" + modificationTimestamp = 1522019340 + setStoragePath(FileStorageUtils.getSavePath(user.accountName) + "/nonEmpty.txt") + } + + EspressoIdlingResource.increment() + FileDataStorageManager(user, targetContext.contentResolver).run { + saveNewFile(existingFile) + } + EspressoIdlingResource.decrement() + + val intent = Intent(targetContext, ConflictsResolveActivity::class.java).apply { + putExtra(FileActivity.EXTRA_FILE, newFile) + putExtra(ConflictsResolveActivity.EXTRA_EXISTING_FILE, existingFile) + putExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD_ID, newUpload.uploadId) + } + + launchActivity(intent).use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + sut.listener = OnConflictDecisionMadeListener { decision: Decision? -> + assertEquals(decision, Decision.KEEP_SERVER) + returnCode = true + } + EspressoIdlingResource.decrement() + + onView(ViewMatchers.withId(R.id.right_checkbox)).perform(ViewActions.click()) + val dialog = sut.supportFragmentManager.findFragmentByTag("conflictDialog") as DialogFragment? + val screenShotName = createName(testClassName + "_" + "keepExisting", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(dialog?.requireDialog()?.window?.decorView, screenShotName) + + onView(ViewMatchers.withText("OK")).perform(ViewActions.click()) + assertTrue(returnCode) + } + } + } + } + + @Test + @UiThread + @ScreenshotTest + fun keepNew() { + returnCode = false + + val newUpload = OCUpload( + FileStorageUtils.getSavePath(user.accountName) + "/nonEmpty.txt", + "/newFile.txt", + user.accountName + ) + + val existingFile = OCFile("/newFile.txt").apply { + fileLength = 1024000 + modificationTimestamp = 1582019340 + remoteId = "00000123abc" + } + + val newFile = OCFile("/newFile.txt").apply { + fileLength = 56000 + modificationTimestamp = 1522019340 + setStoragePath(FileStorageUtils.getSavePath(user.accountName) + "/nonEmpty.txt") + } + + val storageManager = FileDataStorageManager(user, targetContext.contentResolver) + storageManager.saveNewFile(existingFile) + + val intent = Intent(targetContext, ConflictsResolveActivity::class.java) + intent.putExtra(FileActivity.EXTRA_FILE, newFile) + intent.putExtra(ConflictsResolveActivity.EXTRA_EXISTING_FILE, existingFile) + intent.putExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD_ID, newUpload.uploadId) + + launchActivity(intent).use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + + sut.listener = OnConflictDecisionMadeListener { decision: Decision? -> + assertEquals(decision, Decision.KEEP_LOCAL) + returnCode = true + } + + EspressoIdlingResource.decrement() + + onView(ViewMatchers.withId(R.id.left_checkbox)).perform(ViewActions.click()) + val dialog = sut.supportFragmentManager.findFragmentByTag("conflictDialog") as DialogFragment? + val screenShotName = createName(testClassName + "_" + "keepNew", "") + screenshotViaName(dialog?.requireDialog()?.window?.decorView, screenShotName) + + onView(ViewMatchers.withText("OK")).perform(ViewActions.click()) + assertTrue(returnCode) + } + } + } + } + + @Test + @UiThread + @ScreenshotTest + fun keepBoth() { + returnCode = false + + val newUpload = OCUpload( + FileStorageUtils.getSavePath(user.accountName) + "/nonEmpty.txt", + "/newFile.txt", + user.accountName + ) + + val existingFile = OCFile("/newFile.txt").apply { + remoteId = "0001" + fileLength = 1024000 + modificationTimestamp = 1582019340 + } + + val newFile = OCFile("/newFile.txt").apply { + fileLength = 56000 + remoteId = "0002" + modificationTimestamp = 1522019340 + setStoragePath(FileStorageUtils.getSavePath(user.accountName) + "/nonEmpty.txt") + } + + val storageManager = FileDataStorageManager(user, targetContext.contentResolver) + storageManager.saveNewFile(existingFile) + + val intent = Intent(targetContext, ConflictsResolveActivity::class.java).apply { + putExtra(FileActivity.EXTRA_FILE, newFile) + putExtra(ConflictsResolveActivity.EXTRA_EXISTING_FILE, existingFile) + putExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD_ID, newUpload.uploadId) + } + + launchActivity(intent).use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + + sut.listener = OnConflictDecisionMadeListener { decision: Decision? -> + assertEquals(decision, Decision.KEEP_BOTH) + returnCode = true + } + + EspressoIdlingResource.decrement() + + onView(ViewMatchers.withId(R.id.right_checkbox)).perform(ViewActions.click()) + onView(ViewMatchers.withId(R.id.left_checkbox)).perform(ViewActions.click()) + + onView(ViewMatchers.withId(R.id.left_checkbox)).perform(ViewActions.click()) + val dialog = sut.supportFragmentManager.findFragmentByTag("conflictDialog") as DialogFragment? + val screenShotName = createName(testClassName + "_" + "keepBoth", "") + screenshotViaName(dialog?.requireDialog()?.window?.decorView, screenShotName) + + onView(ViewMatchers.withText("OK")).perform(ViewActions.click()) + assertTrue(returnCode) + } + } + } + } + + @After + override fun after() { + storageManager.deleteAllFiles() + } +} diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/ContactsPreferenceActivityIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/activity/ContactsPreferenceActivityIT.kt index cabb10a..9faef5a 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/activity/ContactsPreferenceActivityIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/activity/ContactsPreferenceActivityIT.kt @@ -3,24 +3,42 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activity import android.content.Intent -import androidx.test.espresso.intent.rule.IntentsTestRule +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot import com.owncloud.android.AbstractIT import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.ScreenshotTest +import org.junit.After import org.junit.Assert.assertTrue -import org.junit.Rule +import org.junit.Before import org.junit.Test class ContactsPreferenceActivityIT : AbstractIT() { - @get:Rule - var activityRule = IntentsTestRule(ContactsPreferenceActivity::class.java, true, false) + private val testClassName = "com.owncloud.android.ui.activity.ContactsPreferenceActivityIT" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } @Test + @UiThread @ScreenshotTest fun openVCF() { val file = getFile("vcard.vcf") @@ -29,23 +47,34 @@ class ContactsPreferenceActivityIT : AbstractIT() { assertTrue(vcfFile.isDown) - val intent = Intent() - intent.putExtra(ContactsPreferenceActivity.EXTRA_FILE, vcfFile) - intent.putExtra(ContactsPreferenceActivity.EXTRA_USER, user) - val sut = activityRule.launchActivity(intent) + val intent = Intent(targetContext, ContactsPreferenceActivity::class.java).apply { + putExtra(ContactsPreferenceActivity.EXTRA_FILE, vcfFile) + putExtra(ContactsPreferenceActivity.EXTRA_USER, user) + } - shortSleep() - - screenshot(sut) + launchActivity(intent).use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + val screenShotName = createName(testClassName + "_" + "openVCF", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } @Test + @UiThread @ScreenshotTest fun openContactsPreference() { - val sut = activityRule.launchActivity(null) - - shortSleep() - - screenshot(sut) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + val screenShotName = createName(testClassName + "_" + "openContactsPreference", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/DrawerActivityIT.java b/app/src/androidTest/java/com/owncloud/android/ui/activity/DrawerActivityIT.java deleted file mode 100644 index d4473bc..0000000 --- a/app/src/androidTest/java/com/owncloud/android/ui/activity/DrawerActivityIT.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.owncloud.android.ui.activity; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.net.Uri; -import android.os.Bundle; - -import com.nextcloud.client.account.User; -import com.nextcloud.client.account.UserAccountManager; -import com.nextcloud.client.account.UserAccountManagerImpl; -import com.nextcloud.test.RetryTestRule; -import com.owncloud.android.AbstractIT; -import com.owncloud.android.MainApp; -import com.owncloud.android.R; -import com.owncloud.android.lib.common.accounts.AccountUtils; - -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; - -import androidx.test.espresso.intent.rule.IntentsTestRule; - -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.action.ViewActions.click; -import static androidx.test.espresso.matcher.ViewMatchers.withId; -import static androidx.test.espresso.matcher.ViewMatchers.withText; -import static org.hamcrest.Matchers.anyOf; -import static org.junit.Assert.assertEquals; - -public class DrawerActivityIT extends AbstractIT { - @Rule public IntentsTestRule activityRule = new IntentsTestRule<>(FileDisplayActivity.class, - true, - false); - - @Rule - public final RetryTestRule retryTestRule = new RetryTestRule(); - - private static Account account1; - private static User user1; - private static Account account2; - private static String account2Name; - private static String account2DisplayName; - - @BeforeClass - public static void beforeClass() { - Bundle arguments = androidx.test.platform.app.InstrumentationRegistry.getArguments(); - Uri baseUrl = Uri.parse(arguments.getString("TEST_SERVER_URL")); - - AccountManager platformAccountManager = AccountManager.get(targetContext); - UserAccountManager userAccountManager = UserAccountManagerImpl.fromContext(targetContext); - - for (Account account : platformAccountManager.getAccounts()) { - platformAccountManager.removeAccountExplicitly(account); - } - - String loginName = "user1"; - String password = "user1"; - - Account temp = new Account(loginName + "@" + baseUrl, MainApp.getAccountType(targetContext)); - platformAccountManager.addAccountExplicitly(temp, password, null); - platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_ACCOUNT_VERSION, - Integer.toString(UserAccountManager.ACCOUNT_VERSION)); - platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_VERSION, "14.0.0.0"); - platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_BASE_URL, baseUrl.toString()); - platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_USER_ID, loginName); // same as userId - - account1 = userAccountManager.getAccountByName(loginName + "@" + baseUrl); - user1 = userAccountManager.getUser(account1.name).orElseThrow(IllegalAccessError::new); - - loginName = "user2"; - password = "user2"; - - temp = new Account(loginName + "@" + baseUrl, MainApp.getAccountType(targetContext)); - platformAccountManager.addAccountExplicitly(temp, password, null); - platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_ACCOUNT_VERSION, - Integer.toString(UserAccountManager.ACCOUNT_VERSION)); - platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_VERSION, "14.0.0.0"); - platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_BASE_URL, baseUrl.toString()); - platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_USER_ID, loginName); // same as userId - - account2 = userAccountManager.getAccountByName(loginName + "@" + baseUrl); - account2Name = loginName + "@" + baseUrl; - account2DisplayName = "User Two@" + baseUrl; - } - - @Test - public void switchAccountViaAccountList() { - FileDisplayActivity sut = activityRule.launchActivity(null); - - sut.setUser(user1); - - assertEquals(account1, sut.getUser().get().toPlatformAccount()); - - onView(withId(R.id.switch_account_button)).perform(click()); - - onView(anyOf(withText(account2Name), withText(account2DisplayName))).perform(click()); - - waitForIdleSync(); - - assertEquals(account2, sut.getUser().get().toPlatformAccount()); - - onView(withId(R.id.switch_account_button)).perform(click()); - onView(withText(account1.name)).perform(click()); - } -} diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/DrawerActivityIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/activity/DrawerActivityIT.kt new file mode 100644 index 0000000..6574ff7 --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/ui/activity/DrawerActivityIT.kt @@ -0,0 +1,128 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.owncloud.android.ui.activity + +import android.accounts.Account +import android.accounts.AccountManager +import android.net.Uri +import android.view.View +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.platform.app.InstrumentationRegistry +import com.nextcloud.client.account.User +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.account.UserAccountManagerImpl +import com.nextcloud.test.RetryTestRule +import com.owncloud.android.AbstractIT +import com.owncloud.android.MainApp +import com.owncloud.android.R +import com.owncloud.android.lib.common.accounts.AccountUtils +import com.owncloud.android.utils.EspressoIdlingResource +import org.hamcrest.Matchers +import org.junit.Assert +import org.junit.BeforeClass +import org.junit.Rule +import org.junit.Test +import java.util.function.Supplier + +class DrawerActivityIT : AbstractIT() { + @Rule + @JvmField + val retryTestRule = RetryTestRule() + + @Test + @UiThread + fun switchAccountViaAccountList() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + sut.setUser(user1) + + Assert.assertEquals(account1, sut.user.get().toPlatformAccount()) + onView(ViewMatchers.withId(R.id.switch_account_button)).perform(ViewActions.click()) + onView( + Matchers.anyOf( + ViewMatchers.withText(account2Name), + ViewMatchers.withText( + account2DisplayName + ) + ) + ).perform(ViewActions.click()) + Assert.assertEquals(account2, sut.user.get().toPlatformAccount()) + EspressoIdlingResource.decrement() + + onView(ViewMatchers.withId(R.id.switch_account_button)).perform(ViewActions.click()) + onView(ViewMatchers.withText(account1?.name)).perform(ViewActions.click()) + } + } + } + } + + companion object { + private var account1: Account? = null + private var user1: User? = null + private var account2: Account? = null + private var account2Name: String? = null + private var account2DisplayName: String? = null + + @JvmStatic + @BeforeClass + fun beforeClass() { + val arguments = InstrumentationRegistry.getArguments() + val baseUrl = Uri.parse(arguments.getString("TEST_SERVER_URL")) + + val platformAccountManager = AccountManager.get(targetContext) + val userAccountManager: UserAccountManager = UserAccountManagerImpl.fromContext(targetContext) + + for (account in platformAccountManager.accounts) { + platformAccountManager.removeAccountExplicitly(account) + } + + var loginName = "user1" + var password = "user1" + + var temp = Account("$loginName@$baseUrl", MainApp.getAccountType(targetContext)) + platformAccountManager.addAccountExplicitly(temp, password, null) + platformAccountManager.setUserData( + temp, + AccountUtils.Constants.KEY_OC_ACCOUNT_VERSION, + UserAccountManager.ACCOUNT_VERSION.toString() + ) + platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_VERSION, "14.0.0.0") + platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_BASE_URL, baseUrl.toString()) + platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_USER_ID, loginName) // same as userId + + account1 = userAccountManager.getAccountByName("$loginName@$baseUrl") + user1 = userAccountManager.getUser(account1!!.name) + .orElseThrow(Supplier { IllegalAccessError() }) + + loginName = "user2" + password = "user2" + + temp = Account("$loginName@$baseUrl", MainApp.getAccountType(targetContext)) + platformAccountManager.addAccountExplicitly(temp, password, null) + platformAccountManager.setUserData( + temp, + AccountUtils.Constants.KEY_OC_ACCOUNT_VERSION, + UserAccountManager.ACCOUNT_VERSION.toString() + ) + platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_VERSION, "14.0.0.0") + platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_OC_BASE_URL, baseUrl.toString()) + platformAccountManager.setUserData(temp, AccountUtils.Constants.KEY_USER_ID, loginName) // same as userId + + account2 = userAccountManager.getAccountByName("$loginName@$baseUrl") + account2Name = "$loginName@$baseUrl" + account2DisplayName = "User Two@$baseUrl" + } + } +} diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/FileDisplayActivityTest.java b/app/src/androidTest/java/com/owncloud/android/ui/activity/FileDisplayActivityTest.java index 51482ff..8f65ddc 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/activity/FileDisplayActivityTest.java +++ b/app/src/androidTest/java/com/owncloud/android/ui/activity/FileDisplayActivityTest.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Unpublished - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activity; @@ -27,8 +27,8 @@ public class FileDisplayActivityTest extends AbstractIT { InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { Activity activity = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(RESUMED).iterator().next(); - if (activity instanceof WhatsNewActivity) { - activity.onBackPressed(); + if (activity instanceof WhatsNewActivity whatsNewActivity) { + whatsNewActivity.getOnBackPressedDispatcher().onBackPressed(); } }); scenario.recreate(); diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/FolderPickerActivityIT.java b/app/src/androidTest/java/com/owncloud/android/ui/activity/FolderPickerActivityIT.java deleted file mode 100644 index fa29777..0000000 --- a/app/src/androidTest/java/com/owncloud/android/ui/activity/FolderPickerActivityIT.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2019 Kilian Périsset - * SPDX-FileCopyrightText: 2019 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.owncloud.android.ui.activity; - -import android.content.Intent; - -import com.owncloud.android.AbstractIT; -import com.owncloud.android.R; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.utils.ScreenshotTest; - -import org.junit.Assert; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.rule.ActivityTestRule; - -@RunWith(AndroidJUnit4.class) -//@LargeTest -public class FolderPickerActivityIT extends AbstractIT { - @Rule - public ActivityTestRule activityRule = - new ActivityTestRule<>(FolderPickerActivity.class); - - @Test - public void getActivityFile() { - // Arrange - FolderPickerActivity targetActivity = activityRule.getActivity(); - OCFile origin = new OCFile("/test/file.test"); - origin.setRemotePath("/remotePath/test"); - - // Act - targetActivity.setFile(origin); - OCFile target = targetActivity.getFile(); - - // Assert - Assert.assertEquals(origin, target); - } - - @Test - public void getParentFolder_isNotRootFolder() { - // Arrange - FolderPickerActivity targetActivity = activityRule.getActivity(); - OCFile origin = new OCFile("/test/"); - origin.setFileId(1); - origin.setRemotePath("/test/"); - origin.setStoragePath("/test/"); - origin.setFolder(); - - // Act - targetActivity.setFile(origin); - OCFile target = targetActivity.getCurrentFolder(); - - // Assert - Assert.assertEquals(origin, target); - } - - @Test - public void getParentFolder_isRootFolder() { - // Arrange - FolderPickerActivity targetActivity = activityRule.getActivity(); - OCFile origin = new OCFile("/"); - origin.setFileId(1); - origin.setRemotePath("/"); - origin.setStoragePath("/"); - origin.setFolder(); - - // Act - targetActivity.setFile(origin); - OCFile target = targetActivity.getCurrentFolder(); - - // Assert - Assert.assertEquals(origin, target); - } - - @Test - public void nullFile() { - // Arrange - FolderPickerActivity targetActivity = activityRule.getActivity(); - OCFile rootFolder = targetActivity.getStorageManager().getFileByPath(OCFile.ROOT_PATH); - - // Act - targetActivity.setFile(null); - OCFile target = targetActivity.getCurrentFolder(); - - // Assert - Assert.assertEquals(rootFolder, target); - } - - @Test - public void getParentFolder() { - // Arrange - FolderPickerActivity targetActivity = activityRule.getActivity(); - OCFile origin = new OCFile("/test/file.test"); - origin.setRemotePath("/test/file.test"); - - OCFile target = new OCFile("/test/"); - - // Act - targetActivity.setFile(origin); - - // Assert - Assert.assertEquals(origin, target); - } - - @Test - @ScreenshotTest - public void open() { - FolderPickerActivity sut = activityRule.getActivity(); - OCFile origin = new OCFile("/test/file.txt"); - sut.setFile(origin); - - sut.runOnUiThread(() -> { - sut.findViewById(R.id.folder_picker_btn_copy).requestFocus(); - }); - waitForIdleSync(); - screenshot(sut); - } - - @Test - @ScreenshotTest - public void testMoveOrCopy() { - Intent intent = new Intent(); - FolderPickerActivity targetActivity = activityRule.launchActivity(intent); - - waitForIdleSync(); - screenshot(targetActivity); - } - - @Test - @ScreenshotTest - public void testChooseLocationAction() { - Intent intent = new Intent(); - intent.putExtra(FolderPickerActivity.EXTRA_ACTION, FolderPickerActivity.CHOOSE_LOCATION); - FolderPickerActivity targetActivity = activityRule.launchActivity(intent); - - waitForIdleSync(); - screenshot(targetActivity); - } -} diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/FolderPickerActivityIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/activity/FolderPickerActivityIT.kt new file mode 100644 index 0000000..f2e3083 --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/ui/activity/FolderPickerActivityIT.kt @@ -0,0 +1,213 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-FileCopyrightText: 2019 Kilian Périsset + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.owncloud.android.ui.activity + +import android.content.Intent +import android.view.View +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.owncloud.android.AbstractIT +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.utils.EspressoIdlingResource +import com.owncloud.android.utils.ScreenshotTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class FolderPickerActivityIT : AbstractIT() { + private val testClassName = "com.owncloud.android.ui.activity.FolderPickerActivityIT" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } + + @Test + @UiThread + fun getActivityFile() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + val origin = OCFile("/test/file.test").apply { + remotePath = "/remotePath/test" + } + + // Act + sut.file = origin + val target = sut.file + + // Assert + assertEquals(origin, target) + } + } + } + } + + @Test + @UiThread + fun getParentFolder_isNotRootFolder() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + // Arrange + val origin = OCFile("/test/").apply { + fileId = 1 + remotePath = "/test/" + setStoragePath("/test/") + setFolder() + } + + // Act + sut.file = origin + val target = sut.currentFolder + + // Assert + assertEquals(origin, target) + } + } + } + } + + @Test + @UiThread + fun getParentFolder_isRootFolder() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + // Arrange + val origin = OCFile("/").apply { + fileId = 1 + remotePath = "/" + setStoragePath("/") + setFolder() + } + + // Act + sut.file = origin + val target = sut.currentFolder + + // Assert + assertEquals(origin, target) + } + } + } + } + + @Suppress("DEPRECATION") + @Test + @UiThread + fun nullFile() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + // Arrange + val rootFolder = sut.storageManager.getFileByPath(OCFile.ROOT_PATH) + + // Act + sut.file = null + val target = sut.currentFolder + + // Assert + assertEquals(rootFolder, target) + } + } + } + } + + @Test + @UiThread + fun getParentFolder() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + // Arrange + val origin = OCFile("/test/file.test").apply { + remotePath = "/test/file.test" + } + + val target = OCFile("/test/") + + // Act + sut.file = origin + + // Assert + assertEquals(origin, target) + } + } + } + } + + @Test + @UiThread + @ScreenshotTest + fun open() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + val origin = OCFile("/test/file.txt") + sut.file = origin + sut.findViewById(R.id.folder_picker_btn_copy).requestFocus() + val screenShotName = createName(testClassName + "_" + "open", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } + } + + @Test + @UiThread + @ScreenshotTest + fun testMoveOrCopy() { + val intent = Intent(targetContext, FolderPickerActivity::class.java) + launchActivity(intent).use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + val screenShotName = createName(testClassName + "_" + "testMoveOrCopy", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } + } + + @Test + @UiThread + @ScreenshotTest + fun testChooseLocationAction() { + val intent = Intent(targetContext, FolderPickerActivity::class.java).apply { + putExtra(FolderPickerActivity.EXTRA_ACTION, FolderPickerActivity.CHOOSE_LOCATION) + } + + launchActivity(intent).use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + val screenShotName = createName(testClassName + "_" + "testChooseLocationAction", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } + } +} diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/ManageAccountsActivityIT.java b/app/src/androidTest/java/com/owncloud/android/ui/activity/ManageAccountsActivityIT.java deleted file mode 100644 index c06c7cc..0000000 --- a/app/src/androidTest/java/com/owncloud/android/ui/activity/ManageAccountsActivityIT.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.owncloud.android.ui.activity; - -import android.app.Activity; - -import com.nextcloud.client.account.User; -import com.owncloud.android.AbstractIT; -import com.owncloud.android.lib.common.Quota; -import com.owncloud.android.lib.common.UserInfo; -import com.owncloud.android.utils.ScreenshotTest; - -import org.junit.Rule; -import org.junit.Test; - -import java.util.ArrayList; - -import androidx.test.espresso.intent.rule.IntentsTestRule; - -public class ManageAccountsActivityIT extends AbstractIT { - @Rule - public IntentsTestRule activityRule = new IntentsTestRule<>(ManageAccountsActivity.class, - true, - false); - - @Test - @ScreenshotTest - public void open() { - Activity sut = activityRule.launchActivity(null); - - shortSleep(); - - screenshot(sut); - } - - @Test - @ScreenshotTest - public void userInfoDetail() { - ManageAccountsActivity sut = activityRule.launchActivity(null); - - User user = sut.accountManager.getUser(); - - UserInfo userInfo = new UserInfo("test", - true, - "Test User", - "test@nextcloud.com", - "+49 123 456", - "Address 123, Berlin", - "https://www.nextcloud.com", - "https://twitter.com/Nextclouders", - new Quota(), - new ArrayList<>()); - - sut.showUser(user, userInfo); - - shortSleep(); - shortSleep(); - - screenshot(getCurrentActivity()); - } -} diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/ManageAccountsActivityIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/activity/ManageAccountsActivityIT.kt new file mode 100644 index 0000000..dcfe2e6 --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/ui/activity/ManageAccountsActivityIT.kt @@ -0,0 +1,89 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.owncloud.android.ui.activity + +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import com.owncloud.android.AbstractIT +import com.owncloud.android.lib.common.Quota +import com.owncloud.android.lib.common.UserInfo +import com.owncloud.android.utils.EspressoIdlingResource +import com.owncloud.android.utils.ScreenshotTest +import org.junit.After +import org.junit.Before +import org.junit.Test + +class ManageAccountsActivityIT : AbstractIT() { + private val testClassName = "com.owncloud.android.ui.activity.ManageAccountsActivityIT" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } + + @Test + @UiThread + @ScreenshotTest + fun open() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + val screenShotName = createName(testClassName + "_" + "open", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } + } + + @Test + @UiThread + @ScreenshotTest + fun userInfoDetail() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + + val user = sut.accountManager.user + + val userInfo = UserInfo( + "test", + true, + "Test User", + "test@nextcloud.com", + "+49 123 456", + "Address 123, Berlin", + "https://www.nextcloud.com", + "https://twitter.com/Nextclouders", + Quota(), + ArrayList() + ) + EspressoIdlingResource.decrement() + + sut.showUser(user, userInfo) + + val screenShotName = createName(testClassName + "_" + "open", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(getCurrentActivity(), screenShotName) + } + } + } + } +} diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/NotificationsActivityIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/activity/NotificationsActivityIT.kt index 28e9690..d75ab28 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/activity/NotificationsActivityIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/activity/NotificationsActivityIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activity diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/PassCodeActivityIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/activity/PassCodeActivityIT.kt index ff3eb08..9a6befd 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/activity/PassCodeActivityIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/activity/PassCodeActivityIT.kt @@ -1,66 +1,111 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activity import android.content.Intent +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity import androidx.test.espresso.Espresso -import androidx.test.espresso.intent.rule.IntentsTestRule +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot import com.owncloud.android.AbstractIT +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.ScreenshotTest -import org.junit.Rule +import org.junit.After +import org.junit.Before import org.junit.Test class PassCodeActivityIT : AbstractIT() { - @get:Rule - var activityRule = IntentsTestRule(PassCodeActivity::class.java, true, false) + private val testClassName = "com.owncloud.android.ui.activity.PassCodeActivityIT" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } @Test + @UiThread @ScreenshotTest fun check() { - val sut = activityRule.launchActivity(Intent(PassCodeActivity.ACTION_CHECK)) + val intent = Intent(targetContext, PassCodeActivity::class.java).apply { + action = PassCodeActivity.ACTION_CHECK + } - waitForIdleSync() + launchActivity(intent).use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + sut.binding.txt0.clearFocus() + Espresso.closeSoftKeyboard() + EspressoIdlingResource.decrement() - sut.runOnUiThread { sut.binding.txt0.clearFocus() } - Espresso.closeSoftKeyboard() - shortSleep() - waitForIdleSync() - - screenshot(sut) + val screenShotName = createName(testClassName + "_" + "check", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } @Test + @UiThread @ScreenshotTest fun request() { - val sut = activityRule.launchActivity(Intent(PassCodeActivity.ACTION_REQUEST_WITH_RESULT)) + val intent = Intent(targetContext, PassCodeActivity::class.java).apply { + action = PassCodeActivity.ACTION_REQUEST_WITH_RESULT + } - waitForIdleSync() + launchActivity(intent).use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + sut.binding.txt0.clearFocus() + Espresso.closeSoftKeyboard() + EspressoIdlingResource.decrement() - sut.runOnUiThread { sut.binding.txt0.clearFocus() } - Espresso.closeSoftKeyboard() - shortSleep() - waitForIdleSync() - - screenshot(sut) + val screenShotName = createName(testClassName + "_" + "request", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } @Test + @UiThread @ScreenshotTest fun delete() { - val sut = activityRule.launchActivity(Intent(PassCodeActivity.ACTION_CHECK_WITH_RESULT)) + val intent = Intent(targetContext, PassCodeActivity::class.java).apply { + action = PassCodeActivity.ACTION_CHECK_WITH_RESULT + } - waitForIdleSync() + launchActivity(intent).use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + sut.binding.txt0.clearFocus() + Espresso.closeSoftKeyboard() + EspressoIdlingResource.decrement() - sut.runOnUiThread { sut.binding.txt0.clearFocus() } - Espresso.closeSoftKeyboard() - shortSleep() - waitForIdleSync() - - screenshot(sut) + val screenShotName = createName(testClassName + "_" + "delete", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivityIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivityIT.kt index 4b09f50..30ef944 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivityIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivityIT.kt @@ -1,28 +1,53 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activity -import android.app.Activity -import androidx.test.espresso.intent.rule.IntentsTestRule +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot import com.owncloud.android.AbstractIT +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.ScreenshotTest -import org.junit.Rule +import org.junit.After +import org.junit.Before import org.junit.Test class ReceiveExternalFilesActivityIT : AbstractIT() { - @get:Rule - val activityRule = IntentsTestRule(ReceiveExternalFilesActivity::class.java, true, false) + private val testClassName = "com.owncloud.android.ui.activity.ReceiveExternalFilesActivityIT" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } @Test + @UiThread @ScreenshotTest fun open() { - val sut: Activity = activityRule.launchActivity(null) - screenshot(sut) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + val screenShotName = createName(testClassName + "_" + "open", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } @Test diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/UploadFilesActivityIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/activity/UploadFilesActivityIT.kt index 8cc465e..42a9af5 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/activity/UploadFilesActivityIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/activity/UploadFilesActivityIT.kt @@ -1,30 +1,38 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activity import android.content.Intent -import androidx.test.espresso.intent.rule.IntentsTestRule -import com.nextcloud.test.GrantStoragePermissionRule +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import com.nextcloud.test.GrantStoragePermissionRule.Companion.grant import com.owncloud.android.AbstractIT +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.ScreenshotTest import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule import java.io.File class UploadFilesActivityIT : AbstractIT() { - @get:Rule - var activityRule = IntentsTestRule(UploadFilesActivity::class.java, true, false) + private val testClassName = "com.owncloud.android.ui.activity.UploadFilesActivityIT" @get:Rule - var permissionRule = GrantStoragePermissionRule.grant() + var storagePermissionRule: TestRule = grant() private val directories = listOf("A", "B", "C", "D") .map { File("${FileStorageUtils.getTemporalPath(account.name)}${File.separator}$it") } @@ -39,60 +47,133 @@ class UploadFilesActivityIT : AbstractIT() { directories.forEach { it.deleteRecursively() } } + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } + @Test + @UiThread @ScreenshotTest fun noneSelected() { - val sut: UploadFilesActivity = activityRule.launchActivity(null) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() - sut.runOnUiThread { - sut.fileListFragment.setFiles( - directories + - listOf( - File("1.txt"), - File("2.pdf"), - File("3.mp3") + sut.fileListFragment.setFiles( + directories + + listOf( + File("1.txt"), + File("2.pdf"), + File("3.mp3") + ) ) - ) + + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "noneSelected", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut.fileListFragment.binding?.listRoot, screenShotName) + } + } } - - waitForIdleSync() - longSleep() - - screenshot(sut.fileListFragment.binding.listRoot) } @Test + @UiThread @ScreenshotTest fun localFolderPickerMode() { - val sut: UploadFilesActivity = activityRule.launchActivity( - Intent().apply { - putExtra( - UploadFilesActivity.KEY_LOCAL_FOLDER_PICKER_MODE, - true - ) - putExtra( - UploadFilesActivity.REQUEST_CODE_KEY, - FileDisplayActivity.REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM - ) - } - ) - - sut.runOnUiThread { - sut.fileListFragment.setFiles( - directories + val intent = Intent(targetContext, UploadFilesActivity::class.java).apply { + putExtra( + UploadFilesActivity.KEY_LOCAL_FOLDER_PICKER_MODE, + true + ) + putExtra( + UploadFilesActivity.REQUEST_CODE_KEY, + FileDisplayActivity.REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM ) } - waitForIdleSync() + launchActivity(intent).use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() - screenshot(sut) + sut.fileListFragment.setFiles( + directories + ) + + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "localFolderPickerMode", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } - fun fileSelected() { - val sut: UploadFilesActivity = activityRule.launchActivity(null) + @Test + @UiThread + @ScreenshotTest + fun search() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() - // TODO select one + sut.fileListFragment.performSearch("1.txt", arrayListOf(), false) + sut.fileListFragment.setFiles( + directories + + listOf( + File("1.txt"), + File("2.pdf"), + File("3.mp3") + ) + ) - screenshot(sut) + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "search", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } + } + + @Test + @UiThread + @ScreenshotTest + fun selectAll() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + + sut.fileListFragment.setFiles( + listOf( + File("1.txt"), + File("2.pdf"), + File("3.mp3") + ) + ) + + sut.fileListFragment.selectAllFiles(true) + + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "selectAll", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut.fileListFragment.binding?.listRoot, screenShotName) + } + } + } } } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/UserInfoActivityIT.java b/app/src/androidTest/java/com/owncloud/android/ui/activity/UserInfoActivityIT.java deleted file mode 100644 index 46ff6b3..0000000 --- a/app/src/androidTest/java/com/owncloud/android/ui/activity/UserInfoActivityIT.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Andy Scherzinger - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.owncloud.android.ui.activity; - -import android.content.Intent; - -import com.owncloud.android.AbstractIT; -import com.owncloud.android.lib.common.UserInfo; -import com.owncloud.android.utils.ScreenshotTest; - -import org.junit.Rule; -import org.junit.Test; - -import androidx.test.espresso.intent.rule.IntentsTestRule; - -public class UserInfoActivityIT extends AbstractIT { - @Rule - public IntentsTestRule activityRule = new IntentsTestRule<>(UserInfoActivity.class, - true, - false); - - @Test - @ScreenshotTest - public void fullUserInfoDetail() { - final Intent intent = new Intent(targetContext, UserInfoActivity.class); - intent.putExtra(UserInfoActivity.KEY_ACCOUNT, user); - UserInfo userInfo = new UserInfo("test", - true, - "Firstname Familyname", - "oss@rocks.com", - "+49 7613 672 255", - "Awesome Place Av.", - "https://www.nextcloud.com", - "nextclouders", - null, - null - ); - intent.putExtra(UserInfoActivity.KEY_USER_DATA, userInfo); - UserInfoActivity sut = activityRule.launchActivity(intent); - - shortSleep(); - shortSleep(); - - screenshot(sut); - } -} diff --git a/app/src/androidTest/java/com/owncloud/android/ui/activity/UserInfoActivityIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/activity/UserInfoActivityIT.kt new file mode 100644 index 0000000..a5ca4b5 --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/ui/activity/UserInfoActivityIT.kt @@ -0,0 +1,71 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-FileCopyrightText: 2020 Andy Scherzinger + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.owncloud.android.ui.activity + +import android.content.Intent +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import com.owncloud.android.AbstractIT +import com.owncloud.android.lib.common.UserInfo +import com.owncloud.android.utils.EspressoIdlingResource +import com.owncloud.android.utils.ScreenshotTest +import org.junit.After +import org.junit.Before +import org.junit.Test + +class UserInfoActivityIT : AbstractIT() { + private val testClassName = "com.owncloud.android.ui.activity.UserInfoActivityIT" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } + + @Test + @UiThread + @ScreenshotTest + fun fullUserInfoDetail() { + val intent = Intent(targetContext, UserInfoActivity::class.java).apply { + putExtra(UserInfoActivity.KEY_ACCOUNT, user) + + val userInfo = UserInfo( + "test", + true, + "Firstname Familyname", + "oss@rocks.com", + "+49 7613 672 255", + "Awesome Place Av.", + "https://www.nextcloud.com", + "nextclouders", + null, + null + ) + putExtra(UserInfoActivity.KEY_USER_DATA, userInfo) + } + + launchActivity(intent).use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + val screenShotName = createName(testClassName + "_" + "fullUserInfoDetail", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } + } +} diff --git a/app/src/androidTest/java/com/owncloud/android/ui/adapter/OCFileListAdapterIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/adapter/OCFileListAdapterIT.kt index 00e0924..dec36bb 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/adapter/OCFileListAdapterIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/adapter/OCFileListAdapterIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.adapter diff --git a/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java b/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java deleted file mode 100644 index c7e6472..0000000 --- a/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.java +++ /dev/null @@ -1,630 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.owncloud.android.ui.dialog; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.Intent; -import android.net.http.SslCertificate; -import android.net.http.SslError; -import android.os.Looper; -import android.view.View; -import android.view.ViewGroup; -import android.webkit.SslErrorHandler; -import android.widget.TextView; - -import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.google.gson.Gson; -import com.nextcloud.android.common.ui.color.ColorUtil; -import com.nextcloud.android.common.ui.theme.MaterialSchemes; -import com.nextcloud.android.common.ui.theme.MaterialSchemesImpl; -import com.nextcloud.android.lib.resources.profile.Action; -import com.nextcloud.android.lib.resources.profile.HoverCard; -import com.nextcloud.client.account.RegisteredUser; -import com.nextcloud.client.account.Server; -import com.nextcloud.client.account.User; -import com.nextcloud.client.account.UserAccountManager; -import com.nextcloud.client.device.DeviceInfo; -import com.nextcloud.client.documentscan.AppScanOptionalFeature; -import com.nextcloud.ui.ChooseAccountDialogFragment; -import com.nextcloud.ui.fileactions.FileActionsBottomSheet; -import com.nextcloud.utils.EditorUtils; -import com.owncloud.android.AbstractIT; -import com.owncloud.android.MainApp; -import com.owncloud.android.R; -import com.owncloud.android.datamodel.ArbitraryDataProvider; -import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.lib.common.Creator; -import com.owncloud.android.lib.common.DirectEditing; -import com.owncloud.android.lib.common.Editor; -import com.owncloud.android.lib.common.OwnCloudAccount; -import com.owncloud.android.lib.common.accounts.AccountTypeUtils; -import com.owncloud.android.lib.common.accounts.AccountUtils; -import com.owncloud.android.lib.resources.status.CapabilityBooleanType; -import com.owncloud.android.lib.resources.status.OCCapability; -import com.owncloud.android.lib.resources.status.OwnCloudVersion; -import com.owncloud.android.lib.resources.users.Status; -import com.owncloud.android.lib.resources.users.StatusType; -import com.owncloud.android.ui.activity.FileDisplayActivity; -import com.owncloud.android.ui.fragment.OCFileListBottomSheetActions; -import com.owncloud.android.ui.fragment.OCFileListBottomSheetDialog; -import com.owncloud.android.ui.fragment.ProfileBottomSheetDialog; -import com.owncloud.android.utils.MimeTypeUtil; -import com.owncloud.android.utils.ScreenshotTest; -import com.owncloud.android.utils.theme.CapabilityUtils; -import com.owncloud.android.utils.theme.MaterialSchemesProvider; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import org.junit.After; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.Mockito; - -import java.net.URI; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -import androidx.activity.result.contract.ActivityResultContract; -import androidx.annotation.NonNull; -import androidx.fragment.app.DialogFragment; -import androidx.test.espresso.intent.rule.IntentsTestRule; -import kotlin.Unit; - -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; - -public class DialogFragmentIT extends AbstractIT { - - private final String SERVER_URL = "https://nextcloud.localhost"; - - @Rule public IntentsTestRule activityRule = - new IntentsTestRule<>(FileDisplayActivity.class, true, false); - - private FileDisplayActivity getFileDisplayActivity() { - Intent intent = new Intent(targetContext, FileDisplayActivity.class); - return activityRule.launchActivity(intent); - } - - - @After - public void quitLooperIfNeeded() { - if (Looper.myLooper() != null) { - Looper.myLooper().quitSafely(); - } - } - - @Test - @ScreenshotTest - public void testRenameFileDialog() { - if (Looper.myLooper() == null) { - Looper.prepare(); - } - RenameFileDialogFragment dialog = RenameFileDialogFragment.newInstance(new OCFile("/Test/"), - new OCFile("/")); - showDialog(dialog); - } - - @Test - @ScreenshotTest - public void testLoadingDialog() { - LoadingDialog dialog = LoadingDialog.newInstance("Wait…"); - showDialog(dialog); - } - - @Test - @ScreenshotTest - public void testConfirmationDialogWithOneAction() { - ConfirmationDialogFragment dialog = ConfirmationDialogFragment.newInstance(R.string.upload_list_empty_text_auto_upload, new String[]{}, R.string.filedetails_sync_file, R.string.common_ok, -1, -1); - showDialog(dialog); - } - - @Test - @ScreenshotTest - public void testConfirmationDialogWithTwoAction() { - ConfirmationDialogFragment dialog = ConfirmationDialogFragment.newInstance(R.string.upload_list_empty_text_auto_upload, new String[]{}, R.string.filedetails_sync_file, R.string.common_ok, R.string.common_cancel, -1); - showDialog(dialog); - } - - @Test - @ScreenshotTest - public void testConfirmationDialogWithThreeAction() { - ConfirmationDialogFragment dialog = ConfirmationDialogFragment.newInstance(R.string.upload_list_empty_text_auto_upload, new String[]{}, R.string.filedetails_sync_file, R.string.common_ok, R.string.common_cancel, R.string.common_confirm); - showDialog(dialog); - } - - @Test - @ScreenshotTest - public void testConfirmationDialogWithThreeActionRTL() { - enableRTL(); - - ConfirmationDialogFragment dialog = ConfirmationDialogFragment.newInstance(R.string.upload_list_empty_text_auto_upload, new String[] { }, -1, R.string.common_ok, R.string.common_cancel, R.string.common_confirm); - showDialog(dialog); - - resetLocale(); - } - - @Test - @ScreenshotTest - public void testRemoveFileDialog() { - RemoveFilesDialogFragment dialog = RemoveFilesDialogFragment.newInstance(new OCFile("/Test.md")); - showDialog(dialog); - } - - @Test - @ScreenshotTest - public void testRemoveFilesDialog() { - ArrayList toDelete = new ArrayList<>(); - toDelete.add(new OCFile("/Test.md")); - toDelete.add(new OCFile("/Document.odt")); - - RemoveFilesDialogFragment dialog = RemoveFilesDialogFragment.newInstance(toDelete); - showDialog(dialog); - } - - @Test - @ScreenshotTest - public void testRemoveFolderDialog() { - RemoveFilesDialogFragment dialog = RemoveFilesDialogFragment.newInstance(new OCFile("/Folder/")); - showDialog(dialog); - } - - @Test - @ScreenshotTest - public void testRemoveFoldersDialog() { - ArrayList toDelete = new ArrayList<>(); - toDelete.add(new OCFile("/Folder/")); - toDelete.add(new OCFile("/Documents/")); - - RemoveFilesDialogFragment dialog = RemoveFilesDialogFragment.newInstance(toDelete); - showDialog(dialog); - } - - @Test - @ScreenshotTest - public void testNewFolderDialog() { - if (Looper.myLooper() == null) { - Looper.prepare(); - } - CreateFolderDialogFragment sut = CreateFolderDialogFragment.newInstance(new OCFile("/")); - showDialog(sut); - } - - @Test - @ScreenshotTest - public void testEnforcedPasswordDialog() { - if (Looper.myLooper() == null) { - Looper.prepare(); - } - SharePasswordDialogFragment sut = SharePasswordDialogFragment.newInstance(new OCFile("/"), true, false); - showDialog(sut); - } - - @Test - @ScreenshotTest - public void testOptionalPasswordDialog() { - if (Looper.myLooper() == null) { - Looper.prepare(); - } - SharePasswordDialogFragment sut = SharePasswordDialogFragment.newInstance(new OCFile("/"), true, true); - showDialog(sut); - } - - @Test - @ScreenshotTest - public void testAccountChooserDialog() throws AccountUtils.AccountNotFoundException { - FileDisplayActivity activity = getFileDisplayActivity(); - UserAccountManager userAccountManager = activity.getUserAccountManager(); - AccountManager accountManager = AccountManager.get(targetContext); - for (Account account : accountManager.getAccountsByType(MainApp.getAccountType(targetContext))) { - accountManager.removeAccountExplicitly(account); - } - - Account newAccount = new Account("test@https://nextcloud.localhost", MainApp.getAccountType(targetContext)); - accountManager.addAccountExplicitly(newAccount, "password", null); - accountManager.setUserData(newAccount, AccountUtils.Constants.KEY_OC_BASE_URL, SERVER_URL); - accountManager.setUserData(newAccount, AccountUtils.Constants.KEY_USER_ID, "test"); - accountManager.setAuthToken(newAccount, AccountTypeUtils.getAuthTokenTypePass(newAccount.type), "password"); - User newUser = userAccountManager.getUser(newAccount.name).orElseThrow(RuntimeException::new); - userAccountManager.setCurrentOwnCloudAccount(newAccount.name); - - Account newAccount2 = new Account("user1@nextcloud.localhost", MainApp.getAccountType(targetContext)); - accountManager.addAccountExplicitly(newAccount2, "password", null); - accountManager.setUserData(newAccount2, AccountUtils.Constants.KEY_OC_BASE_URL, SERVER_URL); - accountManager.setUserData(newAccount2, AccountUtils.Constants.KEY_USER_ID, "user1"); - accountManager.setUserData(newAccount2, AccountUtils.Constants.KEY_OC_VERSION, "20.0.0"); - accountManager.setAuthToken(newAccount2, AccountTypeUtils.getAuthTokenTypePass(newAccount.type), "password"); - - FileDataStorageManager fileDataStorageManager = new FileDataStorageManager(newUser, - targetContext.getContentResolver()); - - OCCapability capability = new OCCapability(); - capability.setUserStatus(CapabilityBooleanType.TRUE); - capability.setUserStatusSupportsEmoji(CapabilityBooleanType.TRUE); - fileDataStorageManager.saveCapabilities(capability); - - ChooseAccountDialogFragment sut = - ChooseAccountDialogFragment.newInstance(new RegisteredUser(newAccount, - new OwnCloudAccount(newAccount, targetContext), - new Server(URI.create(SERVER_URL), - OwnCloudVersion.nextcloud_20))); - showDialog(activity, sut); - - activity.runOnUiThread(() -> sut.setStatus(new Status(StatusType.DND, - "Busy fixing 🐛…", - "", - -1), - targetContext)); - waitForIdleSync(); - shortSleep(); - screenshot(sut, "dnd"); - - activity.runOnUiThread(() -> sut.setStatus(new Status(StatusType.ONLINE, - "", - "", - -1), - targetContext)); - waitForIdleSync(); - shortSleep(); - screenshot(sut, "online"); - - activity.runOnUiThread(() -> sut.setStatus(new Status(StatusType.ONLINE, - "Let's have some fun", - "🎉", - -1), - targetContext)); - waitForIdleSync(); - shortSleep(); - screenshot(sut, "fun"); - - activity.runOnUiThread(() -> sut.setStatus(new Status(StatusType.OFFLINE, "", "", -1), targetContext)); - waitForIdleSync(); - shortSleep(); - screenshot(sut, "offline"); - - activity.runOnUiThread(() -> sut.setStatus(new Status(StatusType.AWAY, "Vacation", "🌴", -1), targetContext)); - waitForIdleSync(); - shortSleep(); - screenshot(sut, "away"); - } - - @Test - @ScreenshotTest - public void testAccountChooserDialogWithStatusDisabled() throws AccountUtils.AccountNotFoundException { - AccountManager accountManager = AccountManager.get(targetContext); - for (Account account : accountManager.getAccounts()) { - accountManager.removeAccountExplicitly(account); - } - - Account newAccount = new Account("test@https://nextcloud.localhost", MainApp.getAccountType(targetContext)); - accountManager.addAccountExplicitly(newAccount, "password", null); - accountManager.setUserData(newAccount, AccountUtils.Constants.KEY_OC_BASE_URL, SERVER_URL); - accountManager.setUserData(newAccount, AccountUtils.Constants.KEY_USER_ID, "test"); - accountManager.setAuthToken(newAccount, AccountTypeUtils.getAuthTokenTypePass(newAccount.type), "password"); - - FileDisplayActivity fda = getFileDisplayActivity(); - UserAccountManager userAccountManager = fda.getUserAccountManager(); - User newUser = userAccountManager.getUser(newAccount.name).get(); - FileDataStorageManager fileDataStorageManager = new FileDataStorageManager(newUser, - targetContext.getContentResolver()); - - OCCapability capability = new OCCapability(); - capability.setUserStatus(CapabilityBooleanType.FALSE); - - fileDataStorageManager.saveCapabilities(capability); - - ChooseAccountDialogFragment sut = - ChooseAccountDialogFragment.newInstance(new RegisteredUser(newAccount, - new OwnCloudAccount(newAccount, targetContext), - new Server(URI.create(SERVER_URL), - OwnCloudVersion.nextcloud_20))); - showDialog(fda, sut); - } - - @Test - @ScreenshotTest - public void testBottomSheet() { - if (Looper.myLooper() == null) { - Looper.prepare(); - } - - OCFileListBottomSheetActions action = new OCFileListBottomSheetActions() { - - @Override - public void createFolder() { - - } - - @Override - public void uploadFromApp() { - - } - - @Override - public void uploadFiles() { - - } - - @Override - public void newDocument() { - - } - - @Override - public void newSpreadsheet() { - - } - - @Override - public void newPresentation() { - - } - - @Override - public void directCameraUpload() { - - } - - @Override - public void scanDocUpload() { - - } - - @Override - public void showTemplate(Creator creator, String headline) { - - } - - @Override - public void createRichWorkspace() { - - } - }; - - DeviceInfo info = new DeviceInfo(); - OCFile ocFile = new OCFile("/test.md"); - - Intent intent = new Intent(targetContext, FileDisplayActivity.class); - FileDisplayActivity fda = activityRule.launchActivity(intent); - - // add direct editing info - DirectEditing directEditing = new DirectEditing(); - directEditing.getCreators().put("1", new Creator("1", - "text", - "text file", - ".md", - "application/octet-stream", - false)); - - directEditing.getCreators().put("2", new Creator("2", - "md", - "markdown file", - ".md", - "application/octet-stream", - false)); - - directEditing.getEditors().put("text", - new Editor("1", - "Text", - new ArrayList<>(Collections.singletonList(MimeTypeUtil.MIMETYPE_TEXT_MARKDOWN)), - new ArrayList<>(), - false)); - - String json = new Gson().toJson(directEditing); - - new ArbitraryDataProviderImpl(targetContext).storeOrUpdateKeyValue(user.getAccountName(), - ArbitraryDataProvider.DIRECT_EDITING, - json); - - // activate templates - OCCapability capability = fda.getCapabilities(); - capability.setRichDocuments(CapabilityBooleanType.TRUE); - capability.setRichDocumentsDirectEditing(CapabilityBooleanType.TRUE); - capability.setRichDocumentsTemplatesAvailable(CapabilityBooleanType.TRUE); - capability.setAccountName(user.getAccountName()); - - CapabilityUtils.updateCapability(capability); - - AppScanOptionalFeature appScanOptionalFeature = new AppScanOptionalFeature() { - @NonNull - @Override - public ActivityResultContract getScanContract() { - throw new UnsupportedOperationException("Document scan is not available"); - } - }; - - MaterialSchemesProvider materialSchemesProvider = new MaterialSchemesProvider() { - @NonNull - @Override - public MaterialSchemes getMaterialSchemesForUser(@NonNull User user) { - return null; - } - - @NonNull - @Override - public MaterialSchemes getMaterialSchemesForCapability(@NonNull OCCapability capability) { - return null; - } - - @NonNull - @Override - public MaterialSchemes getMaterialSchemesForCurrentUser() { - return new MaterialSchemesImpl(R.color.primary, false); - } - - @NonNull - @Override - public MaterialSchemes getDefaultMaterialSchemes() { - return null; - } - - @NonNull - @Override - public MaterialSchemes getMaterialSchemesForPrimaryBackground() { - return null; - } - }; - - ViewThemeUtils viewThemeUtils = new ViewThemeUtils(materialSchemesProvider.getMaterialSchemesForCurrentUser(), - new ColorUtil(targetContext)); - - EditorUtils editorUtils = new EditorUtils(new ArbitraryDataProviderImpl(targetContext)); - - - OCFileListBottomSheetDialog sut = new OCFileListBottomSheetDialog(fda, - action, - info, - user, - ocFile, - fda.themeUtils, - viewThemeUtils, - editorUtils, - appScanOptionalFeature); - - fda.runOnUiThread(sut::show); - - getInstrumentation().waitForIdleSync(); - shortSleep(); - - sut.getBehavior().setState(BottomSheetBehavior.STATE_EXPANDED); - - getInstrumentation().waitForIdleSync(); - shortSleep(); - - ViewGroup viewGroup = sut.getWindow().findViewById(android.R.id.content); - hideCursors(viewGroup); - - screenshot(Objects.requireNonNull(sut.getWindow()).getDecorView()); - - } - - @Test - @ScreenshotTest - public void testProfileBottomSheet() { - if (Looper.myLooper() == null) { - Looper.prepare(); - } - - // Fixed values for HoverCard - List actions = new ArrayList<>(); - actions.add(new Action("profile", - "View profile", - "https://dev.nextcloud.com/core/img/actions/profile.svg", - "https://dev.nextcloud.com/index.php/u/christine")); - actions.add(new Action("core", - "christine.scott@nextcloud.com", - "https://dev.nextcloud.com/core/img/actions/mail.svg", - "mailto:christine.scott@nextcloud.com")); - - actions.add(new Action("spreed", - "Talk to Christine", - "https://dev.nextcloud.com/apps/spreed/img/app-dark.svg", - "https://dev.nextcloud.com/apps/spreed/?callUser=christine" - )); - - HoverCard hoverCard = new HoverCard("christine", "Christine Scott", actions); - - // show dialog - Intent intent = new Intent(targetContext, FileDisplayActivity.class); - FileDisplayActivity fda = activityRule.launchActivity(intent); - - ProfileBottomSheetDialog sut = new ProfileBottomSheetDialog(fda, - user, - hoverCard, - fda.viewThemeUtils); - - fda.runOnUiThread(sut::show); - - waitForIdleSync(); - - screenshot(sut.getWindow().getDecorView()); - } - - - @Test - @ScreenshotTest - public void testSslUntrustedCertDialog() { - if (Looper.myLooper() == null) { - Looper.prepare(); - } - - final SslCertificate certificate = new SslCertificate("foo", "bar", "2022/01/10", "2022/01/30"); - final SslError sslError = new SslError(SslError.SSL_UNTRUSTED, certificate); - - final SslErrorHandler handler = Mockito.mock(SslErrorHandler.class); - - SslUntrustedCertDialog sut = SslUntrustedCertDialog.newInstanceForEmptySslError(sslError, handler); - showDialog(sut); - } - - - @Test - @ScreenshotTest - public void testStoragePermissionDialog() { - if (Looper.myLooper() == null) { - Looper.prepare(); - } - - StoragePermissionDialogFragment sut = StoragePermissionDialogFragment.Companion.newInstance(false); - showDialog(sut); - } - - @Test - @ScreenshotTest - public void testFileActionsBottomSheet() { - if (Looper.myLooper() == null) { - Looper.prepare(); - } - - OCFile ocFile = new OCFile("/test.md"); - final FileActionsBottomSheet sut = FileActionsBottomSheet.newInstance(ocFile, false); - showDialog(sut); - } - - private FileDisplayActivity showDialog(DialogFragment dialog) { - Intent intent = new Intent(targetContext, FileDisplayActivity.class); - - FileDisplayActivity sut = activityRule.getActivity(); - - if (sut == null) { - sut = activityRule.launchActivity(intent); - } - - return showDialog(sut, dialog); - } - - private FileDisplayActivity showDialog(FileDisplayActivity sut, DialogFragment dialog) { - dialog.show(sut.getSupportFragmentManager(), ""); - - getInstrumentation().waitForIdleSync(); - shortSleep(); - - ViewGroup viewGroup = dialog.requireDialog().getWindow().findViewById(android.R.id.content); - hideCursors(viewGroup); - - screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView()); - - return sut; - } - - private void hideCursors(ViewGroup viewGroup) { - for (int i = 0; i < viewGroup.getChildCount(); i++) { - View child = viewGroup.getChildAt(i); - - if (child instanceof ViewGroup) { - hideCursors((ViewGroup) child); - } - - if (child instanceof TextView) { - ((TextView) child).setCursorVisible(false); - } - } - } -} diff --git a/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.kt new file mode 100644 index 0000000..7613b7b --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.kt @@ -0,0 +1,777 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.owncloud.android.ui.dialog + +import android.accounts.Account +import android.accounts.AccountManager +import android.app.Dialog +import android.content.Intent +import android.net.http.SslCertificate +import android.net.http.SslError +import android.os.Looper +import android.view.ViewGroup +import android.webkit.SslErrorHandler +import android.widget.TextView +import androidx.activity.result.contract.ActivityResultContract +import androidx.annotation.UiThread +import androidx.fragment.app.DialogFragment +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.nextcloud.android.common.ui.color.ColorUtil +import com.nextcloud.android.lib.resources.profile.Action +import com.nextcloud.android.lib.resources.profile.HoverCard +import com.nextcloud.client.account.RegisteredUser +import com.nextcloud.client.account.Server +import com.nextcloud.client.device.DeviceInfo +import com.nextcloud.client.documentscan.AppScanOptionalFeature +import com.nextcloud.ui.ChooseAccountDialogFragment.Companion.newInstance +import com.nextcloud.ui.SetOnlineStatusBottomSheet +import com.nextcloud.ui.fileactions.FileActionsBottomSheet.Companion.newInstance +import com.nextcloud.utils.EditorUtils +import com.owncloud.android.AbstractIT +import com.owncloud.android.MainApp +import com.owncloud.android.R +import com.owncloud.android.authentication.EnforcedServer +import com.owncloud.android.datamodel.ArbitraryDataProvider +import com.owncloud.android.datamodel.ArbitraryDataProviderImpl +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.Creator +import com.owncloud.android.lib.common.DirectEditing +import com.owncloud.android.lib.common.Editor +import com.owncloud.android.lib.common.OwnCloudAccount +import com.owncloud.android.lib.common.accounts.AccountTypeUtils +import com.owncloud.android.lib.common.accounts.AccountUtils +import com.owncloud.android.lib.resources.status.CapabilityBooleanType +import com.owncloud.android.lib.resources.status.OCCapability +import com.owncloud.android.lib.resources.status.OwnCloudVersion +import com.owncloud.android.lib.resources.users.Status +import com.owncloud.android.lib.resources.users.StatusType +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.dialog.LoadingDialog.Companion.newInstance +import com.owncloud.android.ui.dialog.RenameFileDialogFragment.Companion.newInstance +import com.owncloud.android.ui.dialog.SharePasswordDialogFragment.Companion.newInstance +import com.owncloud.android.ui.dialog.SslUntrustedCertDialog.Companion.newInstanceForEmptySslError +import com.owncloud.android.ui.dialog.StoragePermissionDialogFragment.Companion.newInstance +import com.owncloud.android.ui.fragment.OCFileListBottomSheetActions +import com.owncloud.android.ui.fragment.OCFileListBottomSheetDialog +import com.owncloud.android.ui.fragment.ProfileBottomSheetDialog +import com.owncloud.android.utils.EspressoIdlingResource +import com.owncloud.android.utils.MimeTypeUtil +import com.owncloud.android.utils.ScreenshotTest +import com.owncloud.android.utils.theme.CapabilityUtils +import com.owncloud.android.utils.theme.ViewThemeUtils +import io.mockk.mockk +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.net.URI +import java.util.function.Supplier + +@Suppress("TooManyFunctions") +class DialogFragmentIT : AbstractIT() { + private val testClassName = "com.owncloud.android.ui.dialog.DialogFragmentIT" + private val serverUrl = "https://nextcloud.localhost" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun quitLooperIfNeeded() { + Looper.myLooper()?.quitSafely() + } + + @Test + @UiThread + @ScreenshotTest + fun testRenameFileDialog() { + if (Looper.myLooper() == null) { + Looper.prepare() + } + + newInstance( + OCFile("/Test/"), + OCFile("/") + ).run { + showDialog(this) + } + } + + @Test + @UiThread + @ScreenshotTest + fun testLoadingDialog() { + newInstance("Wait…").run { + showDialog(this) + } + } + + @Test + @UiThread + @ScreenshotTest + fun testConfirmationDialogWithOneAction() { + ConfirmationDialogFragment.newInstance( + R.string.upload_list_empty_text_auto_upload, + arrayOf(), + R.string.filedetails_sync_file, + R.string.common_ok, + -1, + -1, + -1 + ).run { + showDialog(this) + } + } + + @Test + @UiThread + @ScreenshotTest + fun testConfirmationDialogWithTwoAction() { + ConfirmationDialogFragment.newInstance( + R.string.upload_list_empty_text_auto_upload, + arrayOf(), + R.string.filedetails_sync_file, + R.string.common_ok, + R.string.common_cancel, + -1, + -1 + ).run { + showDialog(this) + } + } + + @Test + @UiThread + @ScreenshotTest + fun testConfirmationDialogWithThreeAction() { + ConfirmationDialogFragment.newInstance( + R.string.upload_list_empty_text_auto_upload, + arrayOf(), + R.string.filedetails_sync_file, + R.string.common_ok, + R.string.common_cancel, + R.string.common_confirm, + -1 + ).run { + showDialog(this) + } + } + + @Test + @UiThread + @ScreenshotTest + fun testConfirmationDialogWithThreeActionRTL() { + enableRTL() + ConfirmationDialogFragment.newInstance( + R.string.upload_list_empty_text_auto_upload, + arrayOf(), + -1, + R.string.common_ok, + R.string.common_cancel, + R.string.common_confirm, + -1 + ).run { + showDialog(this) + resetLocale() + } + } + + @Test + @UiThread + @ScreenshotTest + fun testRemoveFileDialog() { + RemoveFilesDialogFragment.newInstance(OCFile("/Test.md")).run { + showDialog(this) + } + } + + @Test + @UiThread + @ScreenshotTest + fun testRemoveFilesDialog() { + val toDelete = ArrayList().apply { + add(OCFile("/Test.md")) + add(OCFile("/Document.odt")) + } + + val dialog: RemoveFilesDialogFragment = RemoveFilesDialogFragment.newInstance(toDelete) + showDialog(dialog) + } + + @Test + @UiThread + @ScreenshotTest + fun testRemoveFolderDialog() { + val dialog = RemoveFilesDialogFragment.newInstance(OCFile("/Folder/")) + showDialog(dialog) + } + + @Test + @UiThread + @ScreenshotTest + fun testRemoveFoldersDialog() { + val toDelete = ArrayList() + toDelete.add(OCFile("/Folder/")) + toDelete.add(OCFile("/Documents/")) + + val dialog: RemoveFilesDialogFragment = RemoveFilesDialogFragment.newInstance(toDelete) + showDialog(dialog) + } + + @Test + @UiThread + @ScreenshotTest + fun testNewFolderDialog() { + if (Looper.myLooper() == null) { + Looper.prepare() + } + val sut = CreateFolderDialogFragment.newInstance(OCFile("/")) + showDialog(sut) + } + + @Test + @UiThread + @ScreenshotTest + fun testEnforcedPasswordDialog() { + if (Looper.myLooper() == null) { + Looper.prepare() + } + val sut = newInstance(OCFile("/"), true, false) + showDialog(sut) + } + + @Test + @UiThread + @ScreenshotTest + fun testOptionalPasswordDialog() { + if (Looper.myLooper() == null) { + Looper.prepare() + } + val sut = newInstance(OCFile("/"), true, true) + showDialog(sut) + } + + @Test + @UiThread + @ScreenshotTest + fun testAccountChooserDialog() { + val intent = Intent(targetContext, FileDisplayActivity::class.java) + ActivityScenario.launch(intent).use { scenario -> + scenario.onActivity { activity: FileDisplayActivity -> + EspressoIdlingResource.increment() + + val userAccountManager = activity.userAccountManager + val accountManager = AccountManager.get(targetContext) + for (account in accountManager.getAccountsByType(MainApp.getAccountType(targetContext))) { + accountManager.removeAccountExplicitly(account) + } + + val newAccount = Account("test@https://nextcloud.localhost", MainApp.getAccountType(targetContext)) + accountManager.addAccountExplicitly(newAccount, "password", null) + accountManager.setUserData(newAccount, AccountUtils.Constants.KEY_OC_BASE_URL, serverUrl) + accountManager.setUserData(newAccount, AccountUtils.Constants.KEY_USER_ID, "test") + accountManager.setAuthToken( + newAccount, + AccountTypeUtils.getAuthTokenTypePass(newAccount.type), + "password" + ) + val newUser = userAccountManager.getUser(newAccount.name) + .orElseThrow(Supplier { RuntimeException() }) + userAccountManager.setCurrentOwnCloudAccount(newAccount.name) + + val newAccount2 = Account("user1@nextcloud.localhost", MainApp.getAccountType(targetContext)) + accountManager.addAccountExplicitly(newAccount2, "password", null) + accountManager.setUserData(newAccount2, AccountUtils.Constants.KEY_OC_BASE_URL, serverUrl) + accountManager.setUserData(newAccount2, AccountUtils.Constants.KEY_USER_ID, "user1") + accountManager.setUserData(newAccount2, AccountUtils.Constants.KEY_OC_VERSION, "20.0.0") + accountManager.setAuthToken( + newAccount2, + AccountTypeUtils.getAuthTokenTypePass(newAccount.type), + "password" + ) + + val fileDataStorageManager = FileDataStorageManager( + newUser, + targetContext.contentResolver + ) + + val capability = OCCapability().apply { + userStatus = CapabilityBooleanType.TRUE + userStatusSupportsEmoji = CapabilityBooleanType.TRUE + } + fileDataStorageManager.saveCapabilities(capability) + + EspressoIdlingResource.decrement() + + try { + onIdleSync { + val sut = newInstance( + RegisteredUser( + newAccount, + OwnCloudAccount(newAccount, targetContext), + Server(URI.create(serverUrl), OwnCloudVersion.nextcloud_20) + ) + ) + showDialog(activity, sut) + + sut.setStatus( + Status( + StatusType.DND, + "Busy fixing 🐛…", + "", + -1 + ), + targetContext + ) + screenshot(sut, "dnd") + + sut.setStatus( + Status( + StatusType.ONLINE, + "", + "", + -1 + ), + targetContext + ) + screenshot(sut, "online") + + sut.setStatus( + Status( + StatusType.ONLINE, + "Let's have some fun", + "🎉", + -1 + ), + targetContext + ) + screenshot(sut, "fun") + + sut.setStatus( + Status(StatusType.OFFLINE, "", "", -1), + targetContext + ) + screenshot(sut, "offline") + + sut.setStatus( + Status(StatusType.AWAY, "Vacation", "🌴", -1), + targetContext + ) + screenshot(sut, "away") + } + } catch (e: AccountUtils.AccountNotFoundException) { + throw java.lang.RuntimeException(e) + } + } + } + } + + @Test + @UiThread + @ScreenshotTest + @Throws(AccountUtils.AccountNotFoundException::class) + fun testAccountChooserDialogWithStatusDisabled() { + val accountManager = AccountManager.get(targetContext) + for (account in accountManager.accounts) { + accountManager.removeAccountExplicitly(account) + } + + val newAccount = Account("test@https://nextcloud.localhost", MainApp.getAccountType(targetContext)) + accountManager.addAccountExplicitly(newAccount, "password", null) + accountManager.setUserData(newAccount, AccountUtils.Constants.KEY_OC_BASE_URL, serverUrl) + accountManager.setUserData(newAccount, AccountUtils.Constants.KEY_USER_ID, "test") + accountManager.setAuthToken(newAccount, AccountTypeUtils.getAuthTokenTypePass(newAccount.type), "password") + + launchActivity().use { scenario -> + scenario.onActivity { fda -> + onIdleSync { + EspressoIdlingResource.increment() + val userAccountManager = fda.userAccountManager + val newUser = userAccountManager.getUser(newAccount.name).get() + val fileDataStorageManager = FileDataStorageManager( + newUser, + targetContext.contentResolver + ) + + val capability = OCCapability().apply { + userStatus = CapabilityBooleanType.FALSE + } + + fileDataStorageManager.saveCapabilities(capability) + EspressoIdlingResource.decrement() + + val sut = + newInstance( + RegisteredUser( + newAccount, + OwnCloudAccount(newAccount, targetContext), + Server( + URI.create(serverUrl), + OwnCloudVersion.nextcloud_20 + ) + ) + ) + + onView(isRoot()).check(matches(isDisplayed())) + showDialog(fda, sut) + } + } + } + } + + @Test + @UiThread + @ScreenshotTest + fun testBottomSheet() { + if (Looper.myLooper() == null) { + Looper.prepare() + } + + val action: OCFileListBottomSheetActions = object : OCFileListBottomSheetActions { + override fun createFolder() = Unit + override fun uploadFromApp() = Unit + override fun uploadFiles() = Unit + override fun newDocument() = Unit + override fun newSpreadsheet() = Unit + override fun newPresentation() = Unit + override fun directCameraUpload() = Unit + override fun scanDocUpload() = Unit + override fun showTemplate(creator: Creator?, headline: String?) = Unit + override fun createRichWorkspace() = Unit + } + + val info = DeviceInfo() + val ocFile = OCFile("/test.md").apply { + remoteId = "00000001" + } + + val intent = Intent(targetContext, FileDisplayActivity::class.java) + + launchActivity(intent).use { scenario -> + scenario.onActivity { fda -> + onIdleSync { + EspressoIdlingResource.increment() + + // add direct editing info + var directEditing = DirectEditing() + val creators = directEditing.creators.toMutableMap() + val editors = directEditing.editors.toMutableMap() + + creators.put( + "1", + Creator( + "1", + "text", + "text file", + ".md", + "application/octet-stream", + false + ) + ) + creators.put( + "2", + Creator( + "2", + "md", + "markdown file", + ".md", + "application/octet-stream", + false + ) + ) + editors.put( + "text", + Editor( + "1", + "Text", + ArrayList(mutableListOf(MimeTypeUtil.MIMETYPE_TEXT_MARKDOWN)), + ArrayList(), + false + ) + ) + + directEditing = DirectEditing(editors, creators) + val json = Gson().toJson(directEditing) + + ArbitraryDataProviderImpl(targetContext).storeOrUpdateKeyValue( + user.accountName, + ArbitraryDataProvider.DIRECT_EDITING, + json + ) + + // activate templates + val capability = fda.capabilities.apply { + richDocuments = CapabilityBooleanType.TRUE + richDocumentsDirectEditing = CapabilityBooleanType.TRUE + richDocumentsTemplatesAvailable = CapabilityBooleanType.TRUE + accountName = user.accountName + } + CapabilityUtils.updateCapability(capability) + + val appScanOptionalFeature: AppScanOptionalFeature = object : AppScanOptionalFeature() { + override fun getScanContract(): ActivityResultContract = + throw UnsupportedOperationException("Document scan is not available") + } + + val materialSchemesProvider = getMaterialSchemesProvider() + val viewThemeUtils = ViewThemeUtils( + materialSchemesProvider.getMaterialSchemesForCurrentUser(), + ColorUtil(targetContext) + ) + + val editorUtils = EditorUtils(ArbitraryDataProviderImpl(targetContext)) + + val sut = OCFileListBottomSheetDialog( + fda, + action, + info, + user, + ocFile, + fda.themeUtils, + viewThemeUtils, + editorUtils, + appScanOptionalFeature + ) + EspressoIdlingResource.decrement() + + sut.show() + sut.behavior.setState(BottomSheetBehavior.STATE_EXPANDED) + val viewGroup = sut.window?.findViewById(android.R.id.content) ?: return@onIdleSync + hideCursors(viewGroup) + val screenShotName = createName(testClassName + "_" + "testBottomSheet", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut.window?.decorView, screenShotName) + } + } + } + } + + @Test + @UiThread + @ScreenshotTest + fun testOnlineStatusBottomSheet() { + if (Looper.myLooper() == null) { + Looper.prepare() + } + + // show dialog + val intent = Intent(targetContext, FileDisplayActivity::class.java) + + launchActivity(intent).use { scenario -> + scenario.onActivity { fda -> + onIdleSync { + EspressoIdlingResource.increment() + val sut = SetOnlineStatusBottomSheet( + Status(StatusType.DND, "Focus time", "\uD83E\uDD13", -1) + ) + EspressoIdlingResource.decrement() + sut.show(fda.supportFragmentManager, "set_online_status") + + val screenShotName = createName(testClassName + "_" + "testOnlineStatusBottomSheet", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut.view, screenShotName) + } + } + } + } + + @Test + @UiThread + @ScreenshotTest + fun testProfileBottomSheet() { + if (Looper.myLooper() == null) { + Looper.prepare() + } + + // Fixed values for HoverCard + val actions: MutableList = ArrayList() + actions.add( + Action( + "profile", + "View profile", + "https://dev.nextcloud.com/core/img/actions/profile.svg", + "https://dev.nextcloud.com/index.php/u/christine" + ) + ) + actions.add( + Action( + "core", + "christine.scott@nextcloud.com", + "https://dev.nextcloud.com/core/img/actions/mail.svg", + "mailto:christine.scott@nextcloud.com" + ) + ) + + actions.add( + Action( + "spreed", + "Talk to Christine", + "https://dev.nextcloud.com/apps/spreed/img/app-dark.svg", + "https://dev.nextcloud.com/apps/spreed/?callUser=christine" + ) + ) + + val hoverCard = HoverCard("christine", "Christine Scott", actions) + + // show dialog + val intent = Intent(targetContext, FileDisplayActivity::class.java) + + launchActivity(intent).use { scenario -> + scenario.onActivity { fda -> + onIdleSync { + EspressoIdlingResource.increment() + val sut = ProfileBottomSheetDialog( + fda, + user, + hoverCard, + fda.viewThemeUtils + ) + EspressoIdlingResource.decrement() + sut.show() + + val screenShotName = createName(testClassName + "_" + "testProfileBottomSheet", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut.window?.decorView, screenShotName) + } + } + } + } + + @Test + @UiThread + @ScreenshotTest + fun testSslUntrustedCertDialog() { + if (Looper.myLooper() == null) { + Looper.prepare() + } + + val certificate = SslCertificate("foo", "bar", "2022/01/10", "2022/01/30") + val sslError = SslError(SslError.SSL_UNTRUSTED, certificate) + + val handler = mockk(relaxed = true) + + newInstanceForEmptySslError(sslError, handler).run { + showDialog(this) + } + } + + @Test + @UiThread + @ScreenshotTest + fun testStoragePermissionDialog() { + if (Looper.myLooper() == null) { + Looper.prepare() + } + + newInstance(false).run { + showDialog(this) + } + } + + @Test + @UiThread + @ScreenshotTest + fun testFileActionsBottomSheet() { + if (Looper.myLooper() == null) { + Looper.prepare() + } + + val ocFile = OCFile("/test.md").apply { + remoteId = "0001" + } + + newInstance(ocFile, false).run { + showDialog(this) + } + } + + private fun showDialog(dialog: DialogFragment) { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + onView(isRoot()).check(matches(isDisplayed())) + showDialog(sut, dialog) + } + } + } + } + + private fun showDialog(sut: FileDisplayActivity, dialog: DialogFragment) { + dialog.show(sut.supportFragmentManager, null) + onIdleSync { + val dialogInstance = waitForDialog(dialog) + ?: throw IllegalStateException("Dialog was not created") + + val viewGroup = dialogInstance.window?.findViewById(android.R.id.content) ?: return@onIdleSync + hideCursors(viewGroup) + + onView(isRoot()).check(matches(isDisplayed())) + screenshot(dialogInstance.window?.decorView) + } + } + + private fun waitForDialog(dialogFragment: DialogFragment, timeoutMs: Long = 5000): Dialog? { + val start = System.currentTimeMillis() + while (System.currentTimeMillis() - start < timeoutMs) { + val dialog = dialogFragment.dialog + if (dialog != null) return dialog + Thread.sleep(100) + } + return null + } + + private fun hideCursors(viewGroup: ViewGroup) { + for (i in 0..().apply { + add(EnforcedServer("name", "url")) + add(EnforcedServer("name2", "url1")) + } + + val s = Gson().toJson(t) + val t2 = Gson().fromJson>( + s, + object : TypeToken?>() { + }.type + ) + + val temp = ArrayList() + for (p in t2) { + temp.add(p.name) + } + } +} diff --git a/app/src/androidTest/java/com/owncloud/android/ui/dialog/SendFilesDialogTest.kt b/app/src/androidTest/java/com/owncloud/android/ui/dialog/SendFilesDialogTest.kt index de52287..9e4ec3d 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/dialog/SendFilesDialogTest.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/dialog/SendFilesDialogTest.kt @@ -1,26 +1,36 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.dialog +import androidx.annotation.UiThread import androidx.fragment.app.FragmentManager import androidx.recyclerview.widget.RecyclerView -import androidx.test.espresso.intent.rule.IntentsTestRule -import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot import com.nextcloud.test.TestActivity import com.owncloud.android.AbstractIT import com.owncloud.android.R import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.ScreenshotTest +import org.junit.After import org.junit.Assert -import org.junit.Rule +import org.junit.Before import org.junit.Test class SendFilesDialogTest : AbstractIT() { + private val testClassName = "com.owncloud.android.ui.dialog.SendFilesDialogTest" + companion object { private val FILES_SAME_TYPE = setOf( OCFile("/1.jpg").apply { @@ -43,52 +53,77 @@ class SendFilesDialogTest : AbstractIT() { ) } - @get:Rule - val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false) + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } - private fun showDialog(files: Set): SendFilesDialog { - val activity = testActivityRule.launchActivity(null) + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } - val fm: FragmentManager = activity.supportFragmentManager - val ft = fm.beginTransaction() - ft.addToBackStack(null) + private fun showDialog(files: Set, onComplete: (SendFilesDialog) -> Unit) { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() - val sut = SendFilesDialog.newInstance(files) - sut.show(ft, "TAG_SEND_SHARE_DIALOG") + val fm: FragmentManager = sut.supportFragmentManager + val ft = fm.beginTransaction() + ft.addToBackStack(null) - InstrumentationRegistry.getInstrumentation().waitForIdleSync() - shortSleep() + val dialog = SendFilesDialog.newInstance(files) + dialog.show(ft, "TAG_SEND_SHARE_DIALOG") + onComplete(dialog) - return sut + EspressoIdlingResource.decrement() + } + } + } } @Test + @UiThread + @ScreenshotTest fun showDialog() { - val sut = showDialog(FILES_SAME_TYPE) - val recyclerview: RecyclerView = sut.requireDialog().findViewById(R.id.send_button_recycler_view) - Assert.assertNotNull("Adapter is null", recyclerview.adapter) - Assert.assertNotEquals("Send button list is empty", 0, recyclerview.adapter!!.itemCount) + showDialog(FILES_SAME_TYPE) { sut -> + val recyclerview: RecyclerView = sut.requireDialog().findViewById(R.id.send_button_recycler_view) + Assert.assertNotNull("Adapter is null", recyclerview.adapter) + Assert.assertNotEquals("Send button list is empty", 0, recyclerview.adapter!!.itemCount) + } } @Test + @UiThread @ScreenshotTest fun showDialog_Screenshot() { - val sut = showDialog(FILES_SAME_TYPE) - sut.requireDialog().window?.decorView.let { screenshot(it) } + showDialog(FILES_SAME_TYPE) { sut -> + val screenShotName = createName(testClassName + "_" + "showDialog_Screenshot", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut.requireDialog().window?.decorView, screenShotName) + } } @Test + @UiThread + @ScreenshotTest fun showDialogDifferentTypes() { - val sut = showDialog(FILES_MIXED_TYPE) - val recyclerview: RecyclerView = sut.requireDialog().findViewById(R.id.send_button_recycler_view) - Assert.assertNotNull("Adapter is null", recyclerview.adapter) - Assert.assertNotEquals("Send button list is empty", 0, recyclerview.adapter!!.itemCount) + showDialog(FILES_MIXED_TYPE) { sut -> + val recyclerview: RecyclerView = sut.requireDialog().findViewById(R.id.send_button_recycler_view) + Assert.assertNotNull("Adapter is null", recyclerview.adapter) + Assert.assertNotEquals("Send button list is empty", 0, recyclerview.adapter!!.itemCount) + } } @Test + @UiThread @ScreenshotTest fun showDialogDifferentTypes_Screenshot() { - val sut = showDialog(FILES_MIXED_TYPE) - sut.requireDialog().window?.decorView.let { screenshot(it) } + showDialog(FILES_MIXED_TYPE) { sut -> + val screenShotName = createName(testClassName + "_" + "showDialogDifferentTypes_Screenshot", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut.requireDialog().window?.decorView, screenShotName) + } } } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/dialog/SendShareDialogTest.kt b/app/src/androidTest/java/com/owncloud/android/ui/dialog/SendShareDialogTest.kt index 56d05d6..610aee3 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/dialog/SendShareDialogTest.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/dialog/SendShareDialogTest.kt @@ -1,46 +1,55 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.dialog +import androidx.annotation.UiThread import androidx.fragment.app.FragmentManager -import androidx.test.espresso.intent.rule.IntentsTestRule -import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot import com.nextcloud.test.TestActivity import com.owncloud.android.AbstractIT import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.resources.status.OCCapability +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.ScreenshotTest -import org.junit.Rule import org.junit.Test class SendShareDialogTest : AbstractIT() { - @get:Rule - val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false) + private val testClassName = "com.owncloud.android.ui.dialog.SendShareDialogTest" @Test + @UiThread @ScreenshotTest fun showDialog() { - val activity = testActivityRule.launchActivity(null) + launchActivity().use { scenario -> + scenario.onActivity { activity -> + onIdleSync { + EspressoIdlingResource.increment() + val fm: FragmentManager = activity.supportFragmentManager + val ft = fm.beginTransaction() + ft.addToBackStack(null) - val fm: FragmentManager = activity.supportFragmentManager - val ft = fm.beginTransaction() - ft.addToBackStack(null) + val file = OCFile("/1.jpg").apply { + mimeType = "image/jpg" + } + EspressoIdlingResource.decrement() - val file = OCFile("/1.jpg").apply { - mimeType = "image/jpg" + val sut = SendShareDialog.newInstance(file, false, OCCapability()) + sut.show(ft, "TAG_SEND_SHARE_DIALOG") + val screenShotName = createName(testClassName + "_" + "showDialog", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut.requireDialog().window?.decorView, screenShotName) + } + } } - - val sut = SendShareDialog.newInstance(file, false, OCCapability()) - sut.show(ft, "TAG_SEND_SHARE_DIALOG") - - InstrumentationRegistry.getInstrumentation().waitForIdleSync() - shortSleep() - shortSleep() - sut.requireDialog().window?.decorView.let { screenshot(it) } } } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragmentIT.kt index 42edfb4..11dd954 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/dialog/SetupEncryptionDialogFragmentIT.kt @@ -1,79 +1,101 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.dialog -import androidx.test.espresso.intent.rule.IntentsTestRule -import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot import com.nextcloud.test.TestActivity import com.owncloud.android.AbstractIT +import com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.ScreenshotTest -import org.junit.Rule +import org.junit.After +import org.junit.Before import org.junit.Test class SetupEncryptionDialogFragmentIT : AbstractIT() { - @get:Rule - val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false) + private val testClassName = "com.owncloud.android.ui.dialog.SetupEncryptionDialogFragmentIT" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } @Test + @UiThread @ScreenshotTest fun showMnemonic() { - val activity = testActivityRule.launchActivity(null) + launchActivity().use { scenario -> + scenario.onActivity { activity -> + onIdleSync { + EspressoIdlingResource.increment() + val sut = SetupEncryptionDialogFragment.newInstance(user, 0) - val sut = SetupEncryptionDialogFragment.newInstance(user, 0) + sut.show(activity.supportFragmentManager, "1") - sut.show(activity.supportFragmentManager, "1") + val keyWords = arrayListOf( + "ability", + "able", + "about", + "above", + "absent", + "absorb", + "abstract", + "absurd", + "abuse", + "access", + "accident", + "account", + "accuse" + ) + sut.setMnemonic(keyWords) + sut.showMnemonicInfo() + EspressoIdlingResource.decrement() - val keyWords = arrayListOf( - "ability", - "able", - "about", - "above", - "absent", - "absorb", - "abstract", - "absurd", - "abuse", - "access", - "accident", - "account", - "accuse" - ) - - shortSleep() - - runOnUiThread { - sut.setMnemonic(keyWords) - sut.showMnemonicInfo() + val screenShotName = createName(testClassName + "_" + "showMnemonic", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut.requireDialog().window?.decorView, screenShotName) + } + } } - - waitForIdleSync() - - screenshot(sut.requireDialog().window!!.decorView) } @Test + @UiThread @ScreenshotTest fun error() { - val activity = testActivityRule.launchActivity(null) + launchActivity().use { scenario -> + scenario.onActivity { activity -> + onIdleSync { + EspressoIdlingResource.increment() - val sut = SetupEncryptionDialogFragment.newInstance(user, 0) + val sut = SetupEncryptionDialogFragment.newInstance(user, 0) + sut.show(activity.supportFragmentManager, "1") + sut.errorSavingKeys() - sut.show(activity.supportFragmentManager, "1") + EspressoIdlingResource.decrement() - shortSleep() - - runOnUiThread { - sut.errorSavingKeys() + val screenShotName = createName(testClassName + "_" + "error", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut.requireDialog().window?.decorView, screenShotName) + } + } } - - shortSleep() - waitForIdleSync() - - screenshot(sut.requireDialog().window!!.decorView) } } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.java b/app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.java deleted file mode 100644 index c82db36..0000000 --- a/app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.owncloud.android.ui.dialog; - -import com.owncloud.android.AbstractIT; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.ui.activity.FileDisplayActivity; -import com.owncloud.android.utils.ScreenshotTest; - -import org.junit.Rule; -import org.junit.Test; - -import java.util.Objects; - -import androidx.test.espresso.intent.rule.IntentsTestRule; - -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; - -public class SyncFileNotEnoughSpaceDialogFragmentTest extends AbstractIT { - @Rule public IntentsTestRule activityRule = new IntentsTestRule<>(FileDisplayActivity.class, - true, - false); - - @Test - @ScreenshotTest - public void showNotEnoughSpaceDialogForFolder() { - FileDisplayActivity test = activityRule.launchActivity(null); - OCFile ocFile = new OCFile("/Document/"); - ocFile.setFileLength(5000000); - ocFile.setFolder(); - - SyncFileNotEnoughSpaceDialogFragment dialog = SyncFileNotEnoughSpaceDialogFragment.newInstance(ocFile, 1000); - dialog.show(test.getListOfFilesFragment().getFragmentManager(), "1"); - - getInstrumentation().waitForIdleSync(); - - screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView()); - } - - @Test - @ScreenshotTest - public void showNotEnoughSpaceDialogForFile() { - FileDisplayActivity test = activityRule.launchActivity(null); - OCFile ocFile = new OCFile("/Video.mp4"); - ocFile.setFileLength(1000000); - - SyncFileNotEnoughSpaceDialogFragment dialog = SyncFileNotEnoughSpaceDialogFragment.newInstance(ocFile, 2000); - dialog.show(test.getListOfFilesFragment().getFragmentManager(), "2"); - - getInstrumentation().waitForIdleSync(); - - screenshot(Objects.requireNonNull(dialog.requireDialog().getWindow()).getDecorView()); - } -} diff --git a/app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.kt b/app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.kt new file mode 100644 index 0000000..f4f6246 --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/ui/dialog/SyncFileNotEnoughSpaceDialogFragmentTest.kt @@ -0,0 +1,89 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.dialog + +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import com.owncloud.android.AbstractIT +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragment.Companion.newInstance +import com.owncloud.android.utils.EspressoIdlingResource +import com.owncloud.android.utils.ScreenshotTest +import org.junit.After +import org.junit.Before +import org.junit.Test + +class SyncFileNotEnoughSpaceDialogFragmentTest : AbstractIT() { + private val testClassName = "com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragmentTest" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } + + @Test + @ScreenshotTest + @UiThread + fun showNotEnoughSpaceDialogForFolder() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + val ocFile = OCFile("/Document/").apply { + fileLength = 5000000 + setFolder() + } + + onIdleSync { + EspressoIdlingResource.increment() + newInstance(ocFile, 1000).apply { + show(sut.supportFragmentManager, "1") + } + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "showNotEnoughSpaceDialogForFolder", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } + } + + @Test + @ScreenshotTest + @UiThread + fun showNotEnoughSpaceDialogForFile() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + val ocFile = OCFile("/Video.mp4").apply { + fileLength = 1000000 + } + + onIdleSync { + EspressoIdlingResource.increment() + newInstance(ocFile, 2000).apply { + show(sut.supportFragmentManager, "2") + } + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "showNotEnoughSpaceDialogForFile", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } + } +} diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/AvatarIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/AvatarIT.kt index c1f9c89..55ac749 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/AvatarIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/AvatarIT.kt @@ -1,15 +1,21 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.fragment import android.graphics.BitmapFactory -import androidx.test.espresso.intent.rule.IntentsTestRule -import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot import com.nextcloud.test.TestActivity import com.owncloud.android.AbstractIT import com.owncloud.android.R @@ -17,155 +23,171 @@ import com.owncloud.android.lib.resources.users.StatusType import com.owncloud.android.ui.TextDrawable import com.owncloud.android.utils.BitmapUtils import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.ScreenshotTest -import org.junit.Rule +import org.junit.After +import org.junit.Before import org.junit.Test class AvatarIT : AbstractIT() { - @get:Rule - val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false) + private val testClassName = "com.owncloud.android.ui.fragment.AvatarIT" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } @Test + @UiThread @ScreenshotTest fun showAvatars() { - val avatarRadius = targetContext.resources.getDimension(R.dimen.list_item_avatar_icon_radius) - val width = DisplayUtils.convertDpToPixel(2 * avatarRadius, targetContext) - val sut = testActivityRule.launchActivity(null) - val fragment = AvatarTestFragment() + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() - sut.addFragment(fragment) + val avatarRadius = targetContext.resources.getDimension(R.dimen.list_item_avatar_icon_radius) + val width = DisplayUtils.convertDpToPixel(2 * avatarRadius, targetContext) + val fragment = AvatarTestFragment() - runOnUiThread { - fragment.addAvatar("Admin", avatarRadius, width, targetContext) - fragment.addAvatar("Test Server Admin", avatarRadius, width, targetContext) - fragment.addAvatar("Cormier Paulette", avatarRadius, width, targetContext) - fragment.addAvatar("winston brent", avatarRadius, width, targetContext) - fragment.addAvatar("Baker James Lorena", avatarRadius, width, targetContext) - fragment.addAvatar("Baker James Lorena", avatarRadius, width, targetContext) - fragment.addAvatar("email@nextcloud.localhost", avatarRadius, width, targetContext) + sut.addFragment(fragment) + fragment.run { + addAvatar("Admin", avatarRadius, width, targetContext) + addAvatar("Test Server Admin", avatarRadius, width, targetContext) + addAvatar("Cormier Paulette", avatarRadius, width, targetContext) + addAvatar("winston brent", avatarRadius, width, targetContext) + addAvatar("Baker James Lorena", avatarRadius, width, targetContext) + addAvatar("Baker James Lorena", avatarRadius, width, targetContext) + addAvatar("email@nextcloud.localhost", avatarRadius, width, targetContext) + } + + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "showAvatars", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } } - - shortSleep() - waitForIdleSync() - screenshot(sut) } @Test + @UiThread @ScreenshotTest fun showAvatarsWithStatus() { - val avatarRadius = targetContext.resources.getDimension(R.dimen.list_item_avatar_icon_radius) - val width = DisplayUtils.convertDpToPixel(2 * avatarRadius, targetContext) - val sut = testActivityRule.launchActivity(null) - val fragment = AvatarTestFragment() + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() - val paulette = BitmapFactory.decodeFile(getFile("paulette.jpg").absolutePath) - val christine = BitmapFactory.decodeFile(getFile("christine.jpg").absolutePath) - val textBitmap = BitmapUtils.drawableToBitmap(TextDrawable.createNamedAvatar("Admin", avatarRadius)) + val avatarRadius = targetContext.resources.getDimension(R.dimen.list_item_avatar_icon_radius) + val width = DisplayUtils.convertDpToPixel(2 * avatarRadius, targetContext) + val fragment = AvatarTestFragment() - sut.addFragment(fragment) + val paulette = BitmapFactory.decodeFile(getFile("paulette.jpg").absolutePath) + val christine = BitmapFactory.decodeFile(getFile("christine.jpg").absolutePath) + val textBitmap = BitmapUtils.drawableToBitmap(TextDrawable.createNamedAvatar("Admin", avatarRadius)) - runOnUiThread { - fragment.addBitmap( - BitmapUtils.createAvatarWithStatus(paulette, StatusType.ONLINE, "😘", targetContext), - width * 2, - 1, - targetContext - ) + sut.addFragment(fragment) - fragment.addBitmap( - BitmapUtils.createAvatarWithStatus(christine, StatusType.ONLINE, "☁️", targetContext), - width * 2, - 1, - targetContext - ) + fragment.run { + addBitmap( + BitmapUtils.createAvatarWithStatus(paulette, StatusType.ONLINE, "😘", targetContext), + width * 2, + 1, + targetContext + ) + addBitmap( + BitmapUtils.createAvatarWithStatus(christine, StatusType.ONLINE, "☁️", targetContext), + width * 2, + 1, + targetContext + ) + addBitmap( + BitmapUtils.createAvatarWithStatus(christine, StatusType.ONLINE, "🌴️", targetContext), + width * 2, + 1, + targetContext + ) + addBitmap( + BitmapUtils.createAvatarWithStatus(christine, StatusType.ONLINE, "", targetContext), + width * 2, + 1, + targetContext + ) + addBitmap( + BitmapUtils.createAvatarWithStatus(paulette, StatusType.DND, "", targetContext), + width * 2, + 1, + targetContext + ) + addBitmap( + BitmapUtils.createAvatarWithStatus(christine, StatusType.AWAY, "", targetContext), + width * 2, + 1, + targetContext + ) + addBitmap( + BitmapUtils.createAvatarWithStatus(paulette, StatusType.OFFLINE, "", targetContext), + width * 2, + 1, + targetContext + ) + addBitmap( + BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.ONLINE, "😘", targetContext), + width, + 2, + targetContext + ) + addBitmap( + BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.ONLINE, "☁️", targetContext), + width, + 2, + targetContext + ) + addBitmap( + BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.ONLINE, "🌴️", targetContext), + width, + 2, + targetContext + ) + addBitmap( + BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.ONLINE, "", targetContext), + width, + 2, + targetContext + ) + addBitmap( + BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.DND, "", targetContext), + width, + 2, + targetContext + ) + addBitmap( + BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.AWAY, "", targetContext), + width, + 2, + targetContext + ) + addBitmap( + BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.OFFLINE, "", targetContext), + width, + 2, + targetContext + ) + } + EspressoIdlingResource.decrement() - fragment.addBitmap( - BitmapUtils.createAvatarWithStatus(christine, StatusType.ONLINE, "🌴️", targetContext), - width * 2, - 1, - targetContext - ) - - fragment.addBitmap( - BitmapUtils.createAvatarWithStatus(christine, StatusType.ONLINE, "", targetContext), - width * 2, - 1, - targetContext - ) - - fragment.addBitmap( - BitmapUtils.createAvatarWithStatus(paulette, StatusType.DND, "", targetContext), - width * 2, - 1, - targetContext - ) - - fragment.addBitmap( - BitmapUtils.createAvatarWithStatus(christine, StatusType.AWAY, "", targetContext), - width * 2, - 1, - targetContext - ) - - fragment.addBitmap( - BitmapUtils.createAvatarWithStatus(paulette, StatusType.OFFLINE, "", targetContext), - width * 2, - 1, - targetContext - ) - - fragment.addBitmap( - BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.ONLINE, "😘", targetContext), - width, - 2, - targetContext - ) - - fragment.addBitmap( - BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.ONLINE, "☁️", targetContext), - width, - 2, - targetContext - ) - - fragment.addBitmap( - BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.ONLINE, "🌴️", targetContext), - width, - 2, - targetContext - ) - - fragment.addBitmap( - BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.ONLINE, "", targetContext), - width, - 2, - targetContext - ) - - fragment.addBitmap( - BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.DND, "", targetContext), - width, - 2, - targetContext - ) - - fragment.addBitmap( - BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.AWAY, "", targetContext), - width, - 2, - targetContext - ) - - fragment.addBitmap( - BitmapUtils.createAvatarWithStatus(textBitmap, StatusType.OFFLINE, "", targetContext), - width, - 2, - targetContext - ) + val screenShotName = createName(testClassName + "_" + "showAvatarsWithStatus", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } } - - shortSleep() - waitForIdleSync() - screenshot(sut) } } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/AvatarTestFragment.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/AvatarTestFragment.kt index 1999a43..6d21bf1 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/AvatarTestFragment.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/AvatarTestFragment.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.fragment @@ -21,8 +21,8 @@ import com.owncloud.android.R import com.owncloud.android.ui.TextDrawable internal class AvatarTestFragment : Fragment() { - lateinit var list1: LinearLayout - lateinit var list2: LinearLayout + private lateinit var list1: LinearLayout + private lateinit var list2: LinearLayout override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view: View = inflater.inflate(R.layout.avatar_fragment, null) @@ -34,7 +34,7 @@ internal class AvatarTestFragment : Fragment() { } fun addAvatar(name: String, avatarRadius: Float, width: Int, targetContext: Context) { - val margin = padding + val margin = PADDING val imageView = ImageView(targetContext) imageView.setImageDrawable(TextDrawable.createNamedAvatar(name, avatarRadius)) @@ -47,7 +47,7 @@ internal class AvatarTestFragment : Fragment() { } fun addBitmap(bitmap: Bitmap, width: Int, list: Int, targetContext: Context) { - val margin = padding + val margin = PADDING val imageView = ImageView(targetContext) imageView.setImageBitmap(bitmap) @@ -64,6 +64,6 @@ internal class AvatarTestFragment : Fragment() { } companion object { - private const val padding = 10 + private const val PADDING = 10 } } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/BackupListFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/BackupListFragmentIT.kt index c10b69d..8f8580f 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/BackupListFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/BackupListFragmentIT.kt @@ -2,99 +2,159 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2021 Tobias Kaminsky - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.fragment import android.Manifest -import androidx.test.espresso.intent.rule.IntentsTestRule +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.rule.GrantPermissionRule import com.owncloud.android.AbstractIT import com.owncloud.android.R import com.owncloud.android.datamodel.OCFile import com.owncloud.android.ui.activity.ContactsPreferenceActivity import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.ScreenshotTest +import org.junit.After +import org.junit.Before import org.junit.Rule import org.junit.Test class BackupListFragmentIT : AbstractIT() { - @get:Rule - val testActivityRule = IntentsTestRule(ContactsPreferenceActivity::class.java, true, false) - @get:Rule val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR) + private val testClassName = "com.owncloud.android.ui.fragment.BackupListFragmentIT" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } + @Test @ScreenshotTest fun showLoading() { - val sut = testActivityRule.launchActivity(null) - val file = OCFile("/") - val transaction = sut.supportFragmentManager.beginTransaction() + launchActivity().use { scenario -> + scenario.onActivity { sut -> + val file = OCFile("/") + val transaction = sut.supportFragmentManager.beginTransaction() - transaction.replace(R.id.frame_container, BackupListFragment.newInstance(file, user)) - transaction.commit() + onIdleSync { + EspressoIdlingResource.increment() - waitForIdleSync() - screenshot(sut) + transaction.replace(R.id.frame_container, BackupListFragment.newInstance(file, user)) + transaction.commit() + + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "showLoading", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } @Test @ScreenshotTest fun showContactList() { - val sut = testActivityRule.launchActivity(null) - val transaction = sut.supportFragmentManager.beginTransaction() - val file = getFile("vcard.vcf") - val ocFile = OCFile("/vcard.vcf") - ocFile.storagePath = file.absolutePath - ocFile.mimeType = "text/vcard" + launchActivity().use { scenario -> + scenario.onActivity { sut -> + val transaction = sut.supportFragmentManager.beginTransaction() + val file = getFile("vcard.vcf") + val ocFile = OCFile("/vcard.vcf").apply { + storagePath = file.absolutePath + mimeType = "text/vcard" + } - transaction.replace(R.id.frame_container, BackupListFragment.newInstance(ocFile, user)) - transaction.commit() + onIdleSync { + EspressoIdlingResource.increment() - waitForIdleSync() - shortSleep() - screenshot(sut) + transaction.replace(R.id.frame_container, BackupListFragment.newInstance(ocFile, user)) + transaction.commit() + + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "showContactList", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } @Test @ScreenshotTest fun showCalendarList() { - val sut = testActivityRule.launchActivity(null) - val transaction = sut.supportFragmentManager.beginTransaction() - val file = getFile("calendar.ics") - val ocFile = OCFile("/Private calender_2020-09-01_10-45-20.ics.ics") - ocFile.storagePath = file.absolutePath - ocFile.mimeType = "text/calendar" + launchActivity().use { scenario -> + scenario.onActivity { sut -> + val transaction = sut.supportFragmentManager.beginTransaction() + val file = getFile("calendar.ics") + val ocFile = OCFile("/Private calender_2020-09-01_10-45-20.ics.ics").apply { + storagePath = file.absolutePath + mimeType = "text/calendar" + } - transaction.replace(R.id.frame_container, BackupListFragment.newInstance(ocFile, user)) - transaction.commit() + onIdleSync { + EspressoIdlingResource.increment() - waitForIdleSync() - screenshot(sut) + transaction.replace(R.id.frame_container, BackupListFragment.newInstance(ocFile, user)) + transaction.commit() + + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "showCalendarList", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } @Test @ScreenshotTest fun showCalendarAndContactsList() { - val sut = testActivityRule.launchActivity(null) - val transaction = sut.supportFragmentManager.beginTransaction() + launchActivity().use { scenario -> + scenario.onActivity { sut -> + val transaction = sut.supportFragmentManager.beginTransaction() + val calendarFile = getFile("calendar.ics") + val calendarOcFile = OCFile("/Private calender_2020-09-01_10-45-20.ics.ics").apply { + storagePath = calendarFile.absolutePath + mimeType = "text/calendar" + } - val calendarFile = getFile("calendar.ics") - val calendarOcFile = OCFile("/Private calender_2020-09-01_10-45-20.ics") - calendarOcFile.storagePath = calendarFile.absolutePath - calendarOcFile.mimeType = "text/calendar" + val contactFile = getFile("vcard.vcf") + val contactOcFile = OCFile("/vcard.vcf").apply { + storagePath = contactFile.absolutePath + mimeType = "text/vcard" + } - val contactFile = getFile("vcard.vcf") - val contactOcFile = OCFile("/vcard.vcf") - contactOcFile.storagePath = contactFile.absolutePath - contactOcFile.mimeType = "text/vcard" + val files = arrayOf(calendarOcFile, contactOcFile) - val files = arrayOf(calendarOcFile, contactOcFile) - transaction.replace(R.id.frame_container, BackupListFragment.newInstance(files, user)) - transaction.commit() + onIdleSync { + EspressoIdlingResource.increment() - waitForIdleSync() - screenshot(sut) + transaction.replace(R.id.frame_container, BackupListFragment.newInstance(files, user)) + transaction.commit() + + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "showCalendarAndContactsList", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } } 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 222d9e5..0746143 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 @@ -1,14 +1,21 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.fragment -import androidx.test.espresso.intent.rule.IntentsTestRule +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot import com.nextcloud.test.TestActivity import com.nextcloud.ui.ImageDetailFragment import com.owncloud.android.AbstractIT @@ -18,80 +25,111 @@ import com.owncloud.android.lib.resources.activities.model.Activity import com.owncloud.android.lib.resources.activities.model.RichElement import com.owncloud.android.lib.resources.activities.model.RichObject import com.owncloud.android.lib.resources.activities.models.PreviewObject +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.ScreenshotTest -import org.junit.Rule +import org.junit.After +import org.junit.Before import org.junit.Test +import java.io.File import java.util.GregorianCalendar class FileDetailFragmentStaticServerIT : AbstractIT() { - @get:Rule - val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false) + private val testClassName = "com.owncloud.android.ui.fragment.FileDetailFragmentStaticServerIT" - var file = getFile("gps.jpg") - val oCFile = OCFile("/").apply { + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } + + private var file: File = getFile("gps.jpg") + private val oCFile: OCFile = OCFile("/").apply { storagePath = file.absolutePath fileId = 12 fileDataStorageManager.saveFile(this) } @Test + @UiThread @ScreenshotTest fun showFileDetailActivitiesFragment() { - val sut = testActivityRule.launchActivity(null) - sut.addFragment(FileDetailActivitiesFragment.newInstance(oCFile, user)) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + sut.addFragment(FileDetailActivitiesFragment.newInstance(oCFile, user)) + EspressoIdlingResource.decrement() - waitForIdleSync() - shortSleep() - shortSleep() - screenshot(sut) + val screenShotName = createName(testClassName + "_" + "showFileDetailActivitiesFragment", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } @Test + @UiThread @ScreenshotTest fun showFileDetailSharingFragment() { - val sut = testActivityRule.launchActivity(null) - sut.addFragment(FileDetailSharingFragment.newInstance(oCFile, user)) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + sut.addFragment(FileDetailSharingFragment.newInstance(oCFile, user)) + EspressoIdlingResource.decrement() - waitForIdleSync() - shortSleep() - shortSleep() - screenshot(sut) + val screenShotName = createName(testClassName + "_" + "showFileDetailSharingFragment", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } @Test + @UiThread @ScreenshotTest fun showFileDetailDetailsFragment() { - val activity = testActivityRule.launchActivity(null) - val sut = ImageDetailFragment.newInstance(oCFile, user) - activity.addFragment(sut) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + val fragment = ImageDetailFragment.newInstance(oCFile, user).apply { + hideMap() + } + sut.addFragment(fragment) + EspressoIdlingResource.decrement() - shortSleep() - shortSleep() - waitForIdleSync() - - activity.runOnUiThread { - sut.hideMap() + val screenShotName = createName(testClassName + "_" + "showFileDetailDetailsFragment", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } } - - screenshot(activity) } @Test + @UiThread @ScreenshotTest @Suppress("MagicNumber") fun showDetailsActivities() { - val date = GregorianCalendar() - date.set(2005, 4, 17, 10, 35, 30) // random date + val date = GregorianCalendar().apply { + set(2005, 4, 17, 10, 35, 30) + } - val richObjectList: ArrayList = ArrayList() - richObjectList.add(RichObject("file", "abc", "text.txt", "/text.txt", "link", "tag")) - richObjectList.add(RichObject("file", "1", "text.txt", "/text.txt", "link", "tag")) + val richObjectList = ArrayList().apply { + add(RichObject("file", "abc", "text.txt", "/text.txt", "link", "tag")) + add(RichObject("file", "1", "text.txt", "/text.txt", "link", "tag")) + } - val previewObjectList1: ArrayList = ArrayList() - previewObjectList1.add(PreviewObject(1, "source", "link", true, "text/plain", "view", "text.txt")) - - val richObjectList2: ArrayList = ArrayList() - richObjectList2.add(RichObject("user", "admin", "Admin", "", "", "")) + val previewObjectList1 = ArrayList().apply { + add(PreviewObject(1, "source", "link", true, "text/plain", "view", "text.txt")) + } val activities = mutableListOf( Activity( @@ -132,67 +170,85 @@ class FileDetailFragmentStaticServerIT : AbstractIT() { ) ) - val sut = FileDetailFragment.newInstance(oCFile, user, 0) - testActivityRule.launchActivity(null).apply { - addFragment(sut) - waitForIdleSync() - runOnUiThread { - sut.fileDetailActivitiesFragment.populateList(activities as List?, true) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + val fragment = FileDetailFragment.newInstance(oCFile, user, 0) + sut.addFragment(fragment) + fragment.fileDetailActivitiesFragment.populateList(activities as List?, true) + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "showDetailsActivities", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } } - longSleep() - screenshot(sut.fileDetailActivitiesFragment.binding.swipeContainingList) } } - // @Test - // @ScreenshotTest - fun showDetailsActivitiesNone() { - val activity = testActivityRule.launchActivity(null) - val sut = FileDetailFragment.newInstance(oCFile, user, 0) - activity.addFragment(sut) - - waitForIdleSync() - - activity.runOnUiThread { - sut.fileDetailActivitiesFragment.populateList(emptyList(), true) - } - - shortSleep() - shortSleep() - screenshot(sut.fileDetailActivitiesFragment.binding.list) - } - @Test + @UiThread + @ScreenshotTest + fun showDetailsActivitiesNone() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + val fragment = FileDetailFragment.newInstance(oCFile, user, 0) + sut.addFragment(fragment) + fragment.fileDetailActivitiesFragment.populateList(emptyList(), true) + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "showDetailsActivitiesNone", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } + } + + @Test + @UiThread @ScreenshotTest fun showDetailsActivitiesError() { - val activity = testActivityRule.launchActivity(null) - val sut = FileDetailFragment.newInstance(oCFile, user, 0) - activity.addFragment(sut) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + val fragment = FileDetailFragment.newInstance(oCFile, user, 0) + sut.addFragment(fragment) + fragment.fileDetailActivitiesFragment.disableLoadingActivities() + fragment.fileDetailActivitiesFragment.setErrorContent( + targetContext.resources.getString(R.string.file_detail_activity_error) + ) + EspressoIdlingResource.decrement() - waitForIdleSync() - - activity.runOnUiThread { - sut.fileDetailActivitiesFragment.disableLoadingActivities() - sut - .fileDetailActivitiesFragment - .setErrorContent(targetContext.resources.getString(R.string.file_detail_activity_error)) + val screenShotName = createName(testClassName + "_" + "showDetailsActivitiesError", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } } - - shortSleep() - shortSleep() - screenshot(sut.fileDetailActivitiesFragment.binding.emptyList.emptyListView) } @Test + @UiThread @ScreenshotTest fun showDetailsSharing() { - val sut = testActivityRule.launchActivity(null) - sut.addFragment(FileDetailFragment.newInstance(oCFile, user, 1)) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + val fragment = FileDetailFragment.newInstance(oCFile, user, 1) + sut.addFragment(fragment) + EspressoIdlingResource.decrement() - waitForIdleSync() - - shortSleep() - shortSleep() - screenshot(sut) + val screenShotName = createName(testClassName + "_" + "showDetailsSharing", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailSharingFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailSharingFragmentIT.kt index 3b794bc..77c204e 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailSharingFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailSharingFragmentIT.kt @@ -4,24 +4,28 @@ * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2021 TSI-mc - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.fragment import android.view.View +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.accessibility.AccessibilityChecks import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.intent.rule.IntentsTestRule import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isNotChecked +import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.withText import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultBaseUtils.matchesCheckNames import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesViews import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.nextcloud.android.lib.resources.files.FileDownloadLimit import com.nextcloud.test.RetryTestRule import com.nextcloud.test.TestActivity import com.owncloud.android.AbstractIT @@ -37,55 +41,68 @@ import com.owncloud.android.lib.resources.shares.OCShare.Companion.READ_PERMISSI import com.owncloud.android.lib.resources.shares.OCShare.Companion.SHARE_PERMISSION_FLAG import com.owncloud.android.lib.resources.shares.ShareType import com.owncloud.android.ui.activity.FileDisplayActivity -import com.owncloud.android.ui.fragment.util.SharingMenuHelper +import com.owncloud.android.ui.fragment.util.SharePermissionManager +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.ScreenshotTest import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.anyOf import org.hamcrest.CoreMatchers.`is` import org.hamcrest.CoreMatchers.not import org.junit.After -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue +import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule import org.junit.Test @Suppress("TooManyFunctions") class FileDetailSharingFragmentIT : AbstractIT() { - @get:Rule - val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false) + private val testClassName = "com.owncloud.android.ui.fragment.FileDetailSharingFragmentIT" @get:Rule val retryRule = RetryTestRule() lateinit var file: OCFile lateinit var folder: OCFile - lateinit var activity: TestActivity + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } @Before fun before() { - activity = testActivityRule.launchActivity(null) - file = OCFile("/test.md").apply { - remoteId = "00000001" - parentId = activity.storageManager.getFileByEncryptedRemotePath("/").fileId - permissions = OCFile.PERMISSION_CAN_RESHARE - fileDataStorageManager.saveFile(this) - } + launchActivity().use { scenario -> + scenario.onActivity { activity -> + file = OCFile("/test.md").apply { + remoteId = "00000001" + parentId = activity.storageManager.getFileByEncryptedRemotePath("/").fileId + permissions = OCFile.PERMISSION_CAN_RESHARE + fileDataStorageManager.saveFile(this) + } - folder = OCFile("/test").apply { - setFolder() - parentId = activity.storageManager.getFileByEncryptedRemotePath("/").fileId - permissions = OCFile.PERMISSION_CAN_RESHARE + folder = OCFile("/test").apply { + setFolder() + parentId = activity.storageManager.getFileByEncryptedRemotePath("/").fileId + permissions = OCFile.PERMISSION_CAN_RESHARE + } + } } } @Test + @UiThread @ScreenshotTest fun listSharesFileNone() { show(file) } @Test + @UiThread @ScreenshotTest fun listSharesFileResharingNotAllowed() { file.permissions = "" @@ -93,127 +110,197 @@ class FileDetailSharingFragmentIT : AbstractIT() { show(file) } + @Test + @UiThread + @ScreenshotTest + fun listSharesDownloadLimit() { + launchActivity().use { scenario -> + scenario.onActivity { activity -> + onIdleSync { + EspressoIdlingResource.increment() + OCShare(file.decryptedRemotePath).apply { + remoteId = 1 + shareType = ShareType.PUBLIC_LINK + token = "AAAAAAAAAAAAAAA" + activity.storageManager.saveShare(this) + } + + OCShare(file.decryptedRemotePath).apply { + remoteId = 2 + shareType = ShareType.PUBLIC_LINK + token = "BBBBBBBBBBBBBBB" + fileDownloadLimit = FileDownloadLimit("BBBBBBBBBBBBBBB", 0, 0) + activity.storageManager.saveShare(this) + } + + OCShare(file.decryptedRemotePath).apply { + remoteId = 3 + shareType = ShareType.PUBLIC_LINK + token = "CCCCCCCCCCCCCCC" + fileDownloadLimit = FileDownloadLimit("CCCCCCCCCCCCCCC", 10, 0) + activity.storageManager.saveShare(this) + } + + OCShare(file.decryptedRemotePath).apply { + remoteId = 4 + shareType = ShareType.PUBLIC_LINK + token = "DDDDDDDDDDDDDDD" + fileDownloadLimit = FileDownloadLimit("DDDDDDDDDDDDDDD", 10, 5) + activity.storageManager.saveShare(this) + } + + OCShare(file.decryptedRemotePath).apply { + remoteId = 5 + shareType = ShareType.PUBLIC_LINK + token = "FFFFFFFFFFFFFFF" + fileDownloadLimit = FileDownloadLimit("FFFFFFFFFFFFFFF", 10, 10) + activity.storageManager.saveShare(this) + } + EspressoIdlingResource.decrement() + + show(file) + } + } + } + } + /** * Use same values as {@link OCFileListFragmentStaticServerIT showSharedFiles } */ @Test + @UiThread @ScreenshotTest @Suppress("MagicNumber") fun listSharesFileAllShareTypes() { - OCShare(file.decryptedRemotePath).apply { - remoteId = 1 - shareType = ShareType.USER - sharedWithDisplayName = "Admin" - permissions = MAXIMUM_PERMISSIONS_FOR_FILE - userId = getUserId(user) - activity.storageManager.saveShare(this) - } + launchActivity().use { scenario -> + scenario.onActivity { activity -> + onIdleSync { + EspressoIdlingResource.increment() + OCShare(file.decryptedRemotePath).apply { + remoteId = 1 + shareType = ShareType.USER + sharedWithDisplayName = "Admin" + permissions = MAXIMUM_PERMISSIONS_FOR_FILE + userId = getUserId(user) + activity.storageManager.saveShare(this) + } - OCShare(file.decryptedRemotePath).apply { - remoteId = 2 - shareType = ShareType.GROUP - sharedWithDisplayName = "Group" - permissions = MAXIMUM_PERMISSIONS_FOR_FILE - userId = getUserId(user) - activity.storageManager.saveShare(this) - } + OCShare(file.decryptedRemotePath).apply { + remoteId = 2 + shareType = ShareType.GROUP + sharedWithDisplayName = "Group" + permissions = MAXIMUM_PERMISSIONS_FOR_FILE + userId = getUserId(user) + activity.storageManager.saveShare(this) + } - OCShare(file.decryptedRemotePath).apply { - remoteId = 3 - shareType = ShareType.EMAIL - sharedWithDisplayName = "admin@nextcloud.localhost" - userId = getUserId(user) - activity.storageManager.saveShare(this) - } + OCShare(file.decryptedRemotePath).apply { + remoteId = 3 + shareType = ShareType.EMAIL + sharedWithDisplayName = "admin@nextcloud.localhost" + userId = getUserId(user) + activity.storageManager.saveShare(this) + } - OCShare(file.decryptedRemotePath).apply { - remoteId = 4 - shareType = ShareType.PUBLIC_LINK - label = "Customer" - activity.storageManager.saveShare(this) - } + OCShare(file.decryptedRemotePath).apply { + remoteId = 4 + shareType = ShareType.PUBLIC_LINK + label = "Customer" + activity.storageManager.saveShare(this) + } - OCShare(file.decryptedRemotePath).apply { - remoteId = 5 - shareType = ShareType.PUBLIC_LINK - label = "Colleagues" - activity.storageManager.saveShare(this) - } + OCShare(file.decryptedRemotePath).apply { + remoteId = 5 + shareType = ShareType.PUBLIC_LINK + label = "Colleagues" + activity.storageManager.saveShare(this) + } - OCShare(file.decryptedRemotePath).apply { - remoteId = 6 - shareType = ShareType.FEDERATED - sharedWithDisplayName = "admin@nextcloud.localhost" - permissions = OCShare.FEDERATED_PERMISSIONS_FOR_FILE - userId = getUserId(user) - activity.storageManager.saveShare(this) - } + OCShare(file.decryptedRemotePath).apply { + remoteId = 6 + shareType = ShareType.FEDERATED + sharedWithDisplayName = "admin@nextcloud.localhost" + permissions = OCShare.FEDERATED_PERMISSIONS_FOR_FILE + userId = getUserId(user) + activity.storageManager.saveShare(this) + } - OCShare(file.decryptedRemotePath).apply { - remoteId = 7 - shareType = ShareType.CIRCLE - sharedWithDisplayName = "Personal team" - permissions = SHARE_PERMISSION_FLAG - userId = getUserId(user) - activity.storageManager.saveShare(this) - } + OCShare(file.decryptedRemotePath).apply { + remoteId = 7 + shareType = ShareType.CIRCLE + sharedWithDisplayName = "Personal team" + permissions = SHARE_PERMISSION_FLAG + userId = getUserId(user) + activity.storageManager.saveShare(this) + } - OCShare(file.decryptedRemotePath).apply { - remoteId = 8 - shareType = ShareType.CIRCLE - sharedWithDisplayName = "Public team" - permissions = SHARE_PERMISSION_FLAG - userId = getUserId(user) - activity.storageManager.saveShare(this) - } + OCShare(file.decryptedRemotePath).apply { + remoteId = 8 + shareType = ShareType.CIRCLE + sharedWithDisplayName = "Public team" + permissions = SHARE_PERMISSION_FLAG + userId = getUserId(user) + activity.storageManager.saveShare(this) + } - OCShare(file.decryptedRemotePath).apply { - remoteId = 9 - shareType = ShareType.CIRCLE - sharedWithDisplayName = "Closed team" - permissions = SHARE_PERMISSION_FLAG - userId = getUserId(user) - activity.storageManager.saveShare(this) - } + OCShare(file.decryptedRemotePath).apply { + remoteId = 9 + shareType = ShareType.CIRCLE + sharedWithDisplayName = "Closed team" + permissions = SHARE_PERMISSION_FLAG + userId = getUserId(user) + activity.storageManager.saveShare(this) + } - OCShare(file.decryptedRemotePath).apply { - remoteId = 10 - shareType = ShareType.CIRCLE - sharedWithDisplayName = "Secret team" - permissions = SHARE_PERMISSION_FLAG - userId = getUserId(user) - activity.storageManager.saveShare(this) - } + OCShare(file.decryptedRemotePath).apply { + remoteId = 10 + shareType = ShareType.CIRCLE + sharedWithDisplayName = "Secret team" + permissions = SHARE_PERMISSION_FLAG + userId = getUserId(user) + activity.storageManager.saveShare(this) + } - OCShare(file.decryptedRemotePath).apply { - remoteId = 11 - shareType = ShareType.ROOM - sharedWithDisplayName = "Admin" - permissions = SHARE_PERMISSION_FLAG - userId = getUserId(user) - activity.storageManager.saveShare(this) - } + OCShare(file.decryptedRemotePath).apply { + remoteId = 11 + shareType = ShareType.ROOM + sharedWithDisplayName = "Admin" + permissions = SHARE_PERMISSION_FLAG + userId = getUserId(user) + activity.storageManager.saveShare(this) + } - OCShare(file.decryptedRemotePath).apply { - remoteId = 12 - shareType = ShareType.ROOM - sharedWithDisplayName = "Meeting" - permissions = SHARE_PERMISSION_FLAG - userId = getUserId(user) - activity.storageManager.saveShare(this) - } + OCShare(file.decryptedRemotePath).apply { + remoteId = 12 + shareType = ShareType.ROOM + sharedWithDisplayName = "Meeting" + permissions = SHARE_PERMISSION_FLAG + userId = getUserId(user) + activity.storageManager.saveShare(this) + } + EspressoIdlingResource.decrement() - show(file) + show(file) + } + } + } } private fun show(file: OCFile) { - val fragment = FileDetailSharingFragment.newInstance(file, user) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() + val fragment = FileDetailSharingFragment.newInstance(file, user) + sut.addFragment(fragment) + EspressoIdlingResource.decrement() - activity.addFragment(fragment) - - waitForIdleSync() - - screenshot(activity) + val screenShotName = createName(testClassName + "_" + "show", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } // public link and email are handled the same way @@ -221,96 +308,101 @@ class FileDetailSharingFragmentIT : AbstractIT() { @Test @Suppress("MagicNumber") fun publicLinkOptionMenuFolderAdvancePermission() { - val sut = FileDetailSharingFragment.newInstance(file, user) - activity.addFragment(sut) - setupSecondaryFragment() - shortSleep() - sut.refreshCapabilitiesFromDB() + launchActivity().use { scenario -> + scenario.onActivity { activity -> + val sut = FileDetailSharingFragment.newInstance(file, user) + activity.addFragment(sut) + onIdleSync { + EspressoIdlingResource.increment() + setupSecondaryFragment() + sut.refreshCapabilitiesFromDB() - val publicShare = OCShare().apply { - isFolder = true - shareType = ShareType.PUBLIC_LINK - permissions = 17 + val publicShare = OCShare().apply { + isFolder = true + shareType = ShareType.PUBLIC_LINK + permissions = 17 + } + + EspressoIdlingResource.decrement() + activity.runOnUiThread { sut.showSharingMenuActionSheet(publicShare) } + + // check if items are visible + onView(ViewMatchers.withId(R.id.menu_share_advanced_permissions)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.menu_share_send_new_email)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.menu_share_send_link)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.menu_share_unshare)).check(matches(isDisplayed())) + + // click event + onView(ViewMatchers.withId(R.id.menu_share_advanced_permissions)).perform(ViewActions.click()) + + // validate view shown on screen + onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.file_request_radio_button)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.share_process_hide_download_checkbox)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.share_process_set_password_switch)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.share_process_change_name_switch)).check(matches(isDisplayed())) + + // read-only + onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isChecked())) + onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isNotChecked())) + onView(ViewMatchers.withId(R.id.file_request_radio_button)).check(matches(isNotChecked())) + goBack() + + // upload and editing + publicShare.permissions = MAXIMUM_PERMISSIONS_FOR_FOLDER + openAdvancedPermissions(sut, publicShare) + onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isNotChecked())) + onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isChecked())) + onView(ViewMatchers.withId(R.id.file_request_radio_button)).check(matches(isNotChecked())) + goBack() + + // file request + publicShare.permissions = 4 + openAdvancedPermissions(sut, publicShare) + onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isNotChecked())) + onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isNotChecked())) + onView(ViewMatchers.withId(R.id.file_request_radio_button)).check(matches(isChecked())) + goBack() + + // password protection + publicShare.shareWith = "someValue" + openAdvancedPermissions(sut, publicShare) + onView(ViewMatchers.withId(R.id.share_process_set_password_switch)).check(matches(isChecked())) + goBack() + + publicShare.shareWith = "" + openAdvancedPermissions(sut, publicShare) + onView(ViewMatchers.withId(R.id.share_process_set_password_switch)).check(matches(isNotChecked())) + goBack() + + // hide download + publicShare.isHideFileDownload = true + publicShare.permissions = MAXIMUM_PERMISSIONS_FOR_FOLDER + openAdvancedPermissions(sut, publicShare) + onView(ViewMatchers.withId(R.id.share_process_hide_download_checkbox)).check(matches(isChecked())) + goBack() + + publicShare.isHideFileDownload = false + openAdvancedPermissions(sut, publicShare) + onView( + ViewMatchers.withId(R.id.share_process_hide_download_checkbox) + ).check(matches(isNotChecked())) + goBack() + + publicShare.expirationDate = 1582019340000 + openAdvancedPermissions(sut, publicShare) + onView(ViewMatchers.withId(R.id.share_process_set_exp_date_switch)).check(matches(isChecked())) + onView(ViewMatchers.withId(R.id.share_process_select_exp_date)).check(matches(not(withText("")))) + goBack() + + publicShare.expirationDate = 0 + openAdvancedPermissions(sut, publicShare) + onView(ViewMatchers.withId(R.id.share_process_set_exp_date_switch)).check(matches(isNotChecked())) + onView(ViewMatchers.withId(R.id.share_process_select_exp_date)).check(matches(withText(""))) + } + } } - - activity.runOnUiThread { sut.showSharingMenuActionSheet(publicShare) } - shortSleep() - waitForIdleSync() - - // check if items are visible - onView(ViewMatchers.withId(R.id.menu_share_advanced_permissions)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.menu_share_send_new_email)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.menu_share_send_link)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.menu_share_unshare)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.menu_share_add_another_link)).check(matches(isDisplayed())) - - // click event - onView(ViewMatchers.withId(R.id.menu_share_advanced_permissions)).perform(ViewActions.click()) - - // validate view shown on screen - onView(ViewMatchers.withId(R.id.share_process_permission_read_only)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.share_process_permission_upload_editing)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.share_process_permission_file_drop)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.share_process_hide_download_checkbox)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.share_process_set_password_switch)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.share_process_change_name_switch)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.share_process_allow_resharing_checkbox)).check(matches(not(isDisplayed()))) - - // read-only - onView(ViewMatchers.withId(R.id.share_process_permission_read_only)).check(matches(isChecked())) - onView(ViewMatchers.withId(R.id.share_process_permission_upload_editing)).check(matches(isNotChecked())) - onView(ViewMatchers.withId(R.id.share_process_permission_file_drop)).check(matches(isNotChecked())) - goBack() - - // upload and editing - publicShare.permissions = MAXIMUM_PERMISSIONS_FOR_FOLDER - openAdvancedPermissions(sut, publicShare) - onView(ViewMatchers.withId(R.id.share_process_permission_read_only)).check(matches(isNotChecked())) - onView(ViewMatchers.withId(R.id.share_process_permission_upload_editing)).check(matches(isChecked())) - onView(ViewMatchers.withId(R.id.share_process_permission_file_drop)).check(matches(isNotChecked())) - goBack() - - // file drop - publicShare.permissions = 4 - openAdvancedPermissions(sut, publicShare) - onView(ViewMatchers.withId(R.id.share_process_permission_read_only)).check(matches(isNotChecked())) - onView(ViewMatchers.withId(R.id.share_process_permission_upload_editing)).check(matches(isNotChecked())) - onView(ViewMatchers.withId(R.id.share_process_permission_file_drop)).check(matches(isChecked())) - goBack() - - // password protection - publicShare.shareWith = "someValue" - openAdvancedPermissions(sut, publicShare) - onView(ViewMatchers.withId(R.id.share_process_set_password_switch)).check(matches(isChecked())) - goBack() - - publicShare.shareWith = "" - openAdvancedPermissions(sut, publicShare) - onView(ViewMatchers.withId(R.id.share_process_set_password_switch)).check(matches(isNotChecked())) - goBack() - - // hide download - publicShare.isHideFileDownload = true - publicShare.permissions = MAXIMUM_PERMISSIONS_FOR_FOLDER - openAdvancedPermissions(sut, publicShare) - onView(ViewMatchers.withId(R.id.share_process_hide_download_checkbox)).check(matches(isChecked())) - goBack() - - publicShare.isHideFileDownload = false - openAdvancedPermissions(sut, publicShare) - onView(ViewMatchers.withId(R.id.share_process_hide_download_checkbox)).check(matches(isNotChecked())) - goBack() - - publicShare.expirationDate = 1582019340000 - openAdvancedPermissions(sut, publicShare) - onView(ViewMatchers.withId(R.id.share_process_set_exp_date_switch)).check(matches(isChecked())) - onView(ViewMatchers.withId(R.id.share_process_select_exp_date)).check(matches(not(withText("")))) - goBack() - - publicShare.expirationDate = 0 - openAdvancedPermissions(sut, publicShare) - onView(ViewMatchers.withId(R.id.share_process_set_exp_date_switch)).check(matches(isNotChecked())) - onView(ViewMatchers.withId(R.id.share_process_select_exp_date)).check(matches(withText(""))) } // public link and email are handled the same way @@ -318,32 +410,43 @@ class FileDetailSharingFragmentIT : AbstractIT() { @Test @Suppress("MagicNumber") fun publicLinkOptionMenuFolderSendNewEmail() { - val sut = FileDetailSharingFragment.newInstance(file, user) - activity.addFragment(sut) - setupSecondaryFragment() - shortSleep() - sut.refreshCapabilitiesFromDB() + launchActivity().use { scenario -> + scenario.onActivity { activity -> + val sut = FileDetailSharingFragment.newInstance(file, user) + activity.addFragment(sut) + onIdleSync { + EspressoIdlingResource.increment() + setupSecondaryFragment() + sut.refreshCapabilitiesFromDB() + EspressoIdlingResource.decrement() - val publicShare = OCShare().apply { - isFolder = true - shareType = ShareType.PUBLIC_LINK - permissions = 17 + val publicShare = OCShare().apply { + isFolder = true + shareType = ShareType.PUBLIC_LINK + permissions = 17 + } + + verifySendNewEmail(sut, publicShare) + } + } } - - verifySendNewEmail(sut, publicShare) } private fun setupSecondaryFragment() { - val parentFolder = OCFile("/") - val secondary = FileDetailFragment.newInstance(file, parentFolder, user) - activity.addSecondaryFragment(secondary, FileDisplayActivity.TAG_LIST_OF_FILES) - activity.addView( - FloatingActionButton(activity).apply { - // needed for some reason - visibility = View.GONE - id = R.id.fab_main + launchActivity().use { scenario -> + scenario.onActivity { activity -> + val parentFolder = OCFile("/") + val secondary = FileDetailFragment.newInstance(file, parentFolder, user) + activity.addSecondaryFragment(secondary, FileDisplayActivity.TAG_LIST_OF_FILES) + activity.addView( + FloatingActionButton(activity).apply { + // needed for some reason + visibility = View.GONE + id = R.id.fab_main + } + ) } - ) + } } // public link and email are handled the same way @@ -351,87 +454,96 @@ class FileDetailSharingFragmentIT : AbstractIT() { @Test @Suppress("MagicNumber") fun publicLinkOptionMenuFileAdvancePermission() { - val sut = FileDetailSharingFragment.newInstance(file, user) - activity.addFragment(sut) - setupSecondaryFragment() - shortSleep() - sut.refreshCapabilitiesFromDB() + launchActivity().use { scenario -> + scenario.onActivity { activity -> + val sut = FileDetailSharingFragment.newInstance(file, user) + activity.addFragment(sut) - val publicShare = OCShare().apply { - isFolder = false - shareType = ShareType.PUBLIC_LINK - permissions = 17 + onIdleSync { + EspressoIdlingResource.increment() + setupSecondaryFragment() + sut.refreshCapabilitiesFromDB() + EspressoIdlingResource.decrement() + + val publicShare = OCShare().apply { + isFolder = false + shareType = ShareType.PUBLIC_LINK + permissions = 17 + } + activity.handler.post { sut.showSharingMenuActionSheet(publicShare) } + + // check if items are visible + onView(ViewMatchers.withId(R.id.menu_share_advanced_permissions)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.menu_share_send_new_email)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.menu_share_send_link)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.menu_share_unshare)).check(matches(isDisplayed())) + + // click event + onView(ViewMatchers.withId(R.id.menu_share_advanced_permissions)).perform(ViewActions.click()) + + // validate view shown on screen + onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isDisplayed())) + onView( + ViewMatchers.withId(R.id.file_request_radio_button) + ).check(matches(not(isDisplayed()))) + onView(ViewMatchers.withId(R.id.share_process_hide_download_checkbox)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.share_process_set_password_switch)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.share_process_change_name_switch)).check(matches(isDisplayed())) + + // read-only + publicShare.permissions = 17 // from server + onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isChecked())) + onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isNotChecked())) + goBack() + + // editing + publicShare.permissions = MAXIMUM_PERMISSIONS_FOR_FILE // from server + openAdvancedPermissions(sut, publicShare) + onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isNotChecked())) + onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isChecked())) + goBack() + + // hide download + publicShare.isHideFileDownload = true + openAdvancedPermissions(sut, publicShare) + onView(ViewMatchers.withId(R.id.share_process_hide_download_checkbox)).check(matches(isChecked())) + goBack() + + publicShare.isHideFileDownload = false + openAdvancedPermissions(sut, publicShare) + onView( + ViewMatchers.withId(R.id.share_process_hide_download_checkbox) + ).check(matches(isNotChecked())) + goBack() + + // password protection + publicShare.isPasswordProtected = true + publicShare.shareWith = "someValue" + openAdvancedPermissions(sut, publicShare) + onView(ViewMatchers.withId(R.id.share_process_set_password_switch)).check(matches(isChecked())) + goBack() + + publicShare.isPasswordProtected = false + publicShare.shareWith = "" + openAdvancedPermissions(sut, publicShare) + onView(ViewMatchers.withId(R.id.share_process_set_password_switch)).check(matches(isNotChecked())) + goBack() + + // expires + publicShare.expirationDate = 1582019340 + openAdvancedPermissions(sut, publicShare) + onView(ViewMatchers.withId(R.id.share_process_set_exp_date_switch)).check(matches(isChecked())) + onView(ViewMatchers.withId(R.id.share_process_select_exp_date)).check(matches(not(withText("")))) + goBack() + + publicShare.expirationDate = 0 + openAdvancedPermissions(sut, publicShare) + onView(ViewMatchers.withId(R.id.share_process_set_exp_date_switch)).check(matches(isNotChecked())) + onView(ViewMatchers.withId(R.id.share_process_select_exp_date)).check(matches(withText(""))) + } + } } - activity.handler.post { sut.showSharingMenuActionSheet(publicShare) } - waitForIdleSync() - - // check if items are visible - onView(ViewMatchers.withId(R.id.menu_share_advanced_permissions)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.menu_share_send_new_email)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.menu_share_send_link)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.menu_share_unshare)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.menu_share_add_another_link)).check(matches(isDisplayed())) - - // click event - onView(ViewMatchers.withId(R.id.menu_share_advanced_permissions)).perform(ViewActions.click()) - - // validate view shown on screen - onView(ViewMatchers.withId(R.id.share_process_permission_read_only)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.share_process_permission_upload_editing)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.share_process_permission_file_drop)).check(matches(not(isDisplayed()))) - onView(ViewMatchers.withId(R.id.share_process_hide_download_checkbox)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.share_process_set_password_switch)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.share_process_change_name_switch)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.share_process_allow_resharing_checkbox)).check(matches(not(isDisplayed()))) - - // read-only - publicShare.permissions = 17 // from server - onView(ViewMatchers.withId(R.id.share_process_permission_read_only)).check(matches(isChecked())) - onView(ViewMatchers.withId(R.id.share_process_permission_upload_editing)).check(matches(isNotChecked())) - goBack() - - // editing - publicShare.permissions = MAXIMUM_PERMISSIONS_FOR_FILE // from server - openAdvancedPermissions(sut, publicShare) - onView(ViewMatchers.withId(R.id.share_process_permission_read_only)).check(matches(isNotChecked())) - onView(ViewMatchers.withId(R.id.share_process_permission_upload_editing)).check(matches(isChecked())) - goBack() - - // hide download - publicShare.isHideFileDownload = true - openAdvancedPermissions(sut, publicShare) - onView(ViewMatchers.withId(R.id.share_process_hide_download_checkbox)).check(matches(isChecked())) - goBack() - - publicShare.isHideFileDownload = false - openAdvancedPermissions(sut, publicShare) - onView(ViewMatchers.withId(R.id.share_process_hide_download_checkbox)).check(matches(isNotChecked())) - goBack() - - // password protection - publicShare.isPasswordProtected = true - publicShare.shareWith = "someValue" - openAdvancedPermissions(sut, publicShare) - onView(ViewMatchers.withId(R.id.share_process_set_password_switch)).check(matches(isChecked())) - goBack() - - publicShare.isPasswordProtected = false - publicShare.shareWith = "" - openAdvancedPermissions(sut, publicShare) - onView(ViewMatchers.withId(R.id.share_process_set_password_switch)).check(matches(isNotChecked())) - goBack() - - // expires - publicShare.expirationDate = 1582019340 - openAdvancedPermissions(sut, publicShare) - onView(ViewMatchers.withId(R.id.share_process_set_exp_date_switch)).check(matches(isChecked())) - onView(ViewMatchers.withId(R.id.share_process_select_exp_date)).check(matches(not(withText("")))) - goBack() - - publicShare.expirationDate = 0 - openAdvancedPermissions(sut, publicShare) - onView(ViewMatchers.withId(R.id.share_process_set_exp_date_switch)).check(matches(isNotChecked())) - onView(ViewMatchers.withId(R.id.share_process_select_exp_date)).check(matches(withText(""))) } // public link and email are handled the same way @@ -439,19 +551,26 @@ class FileDetailSharingFragmentIT : AbstractIT() { @Test @Suppress("MagicNumber") fun publicLinkOptionMenuFileSendNewEmail() { - val sut = FileDetailSharingFragment.newInstance(file, user) - activity.addFragment(sut) - setupSecondaryFragment() - shortSleep() - sut.refreshCapabilitiesFromDB() + launchActivity().use { scenario -> + scenario.onActivity { activity -> + val sut = FileDetailSharingFragment.newInstance(file, user) + activity.addFragment(sut) + onIdleSync { + EspressoIdlingResource.increment() + setupSecondaryFragment() + sut.refreshCapabilitiesFromDB() + EspressoIdlingResource.decrement() - val publicShare = OCShare().apply { - isFolder = false - shareType = ShareType.PUBLIC_LINK - permissions = 17 + val publicShare = OCShare().apply { + isFolder = false + shareType = ShareType.PUBLIC_LINK + permissions = 17 + } + + verifySendNewEmail(sut, publicShare) + } + } } - - verifySendNewEmail(sut, publicShare) } // also applies for @@ -463,79 +582,78 @@ class FileDetailSharingFragmentIT : AbstractIT() { @Test @Suppress("MagicNumber") fun userOptionMenuFileAdvancePermission() { - val sut = FileDetailSharingFragment.newInstance(file, user) - suppressFDFAccessibilityChecks() - activity.addFragment(sut) - setupSecondaryFragment() - shortSleep() - sut.refreshCapabilitiesFromDB() + launchActivity().use { scenario -> + scenario.onActivity { activity -> + val sut = FileDetailSharingFragment.newInstance(file, user) + suppressFDFAccessibilityChecks() + activity.addFragment(sut) - val userShare = OCShare().apply { - isFolder = false - shareType = ShareType.USER - permissions = 17 + onIdleSync { + EspressoIdlingResource.increment() + setupSecondaryFragment() + sut.refreshCapabilitiesFromDB() + EspressoIdlingResource.decrement() + + val userShare = OCShare().apply { + isFolder = false + shareType = ShareType.USER + permissions = 17 + } + + activity.runOnUiThread { sut.showSharingMenuActionSheet(userShare) } + + // check if items are visible + onView(ViewMatchers.withId(R.id.menu_share_advanced_permissions)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.menu_share_send_new_email)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.menu_share_send_link)).check(matches(not(isDisplayed()))) + onView(ViewMatchers.withId(R.id.menu_share_unshare)).check(matches(isDisplayed())) + + // click event + onView(ViewMatchers.withId(R.id.menu_share_advanced_permissions)).perform(ViewActions.click()) + + // validate view shown on screen + onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isDisplayed())) + onView( + ViewMatchers.withId(R.id.file_request_radio_button) + ).check(matches(not(isDisplayed()))) + onView( + ViewMatchers.withId(R.id.share_process_hide_download_checkbox) + ).check(matches(not(isDisplayed()))) + onView( + ViewMatchers.withId(R.id.share_process_set_password_switch) + ).check(matches(not(isDisplayed()))) + onView( + ViewMatchers.withId(R.id.share_process_change_name_switch) + ).check(matches(not(isDisplayed()))) + + // read-only + userShare.permissions = 17 // from server + onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isChecked())) + onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isNotChecked())) + goBack() + + // editing + userShare.permissions = MAXIMUM_PERMISSIONS_FOR_FILE // from server + openAdvancedPermissions(sut, userShare) + onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isNotChecked())) + onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isChecked())) + goBack() + + // set expiration date + userShare.expirationDate = 1582019340000 + openAdvancedPermissions(sut, userShare) + onView(ViewMatchers.withId(R.id.share_process_set_exp_date_switch)).check(matches(isChecked())) + onView(ViewMatchers.withId(R.id.share_process_select_exp_date)).check(matches(not(withText("")))) + goBack() + + userShare.expirationDate = 0 + openAdvancedPermissions(sut, userShare) + onView(ViewMatchers.withId(R.id.share_process_set_exp_date_switch)).check(matches(isNotChecked())) + onView(ViewMatchers.withId(R.id.share_process_select_exp_date)).check(matches(withText(""))) + } + } } - - activity.runOnUiThread { sut.showSharingMenuActionSheet(userShare) } - shortSleep() - waitForIdleSync() - - // check if items are visible - onView(ViewMatchers.withId(R.id.menu_share_advanced_permissions)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.menu_share_send_new_email)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.menu_share_send_link)).check(matches(not(isDisplayed()))) - onView(ViewMatchers.withId(R.id.menu_share_unshare)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.menu_share_add_another_link)).check(matches(not(isDisplayed()))) - - // click event - onView(ViewMatchers.withId(R.id.menu_share_advanced_permissions)).perform(ViewActions.click()) - shortSleep() - waitForIdleSync() - - // validate view shown on screen - onView(ViewMatchers.withId(R.id.share_process_permission_read_only)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.share_process_permission_upload_editing)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.share_process_permission_file_drop)).check(matches(not(isDisplayed()))) - onView(ViewMatchers.withId(R.id.share_process_hide_download_checkbox)).check(matches(not(isDisplayed()))) - onView(ViewMatchers.withId(R.id.share_process_set_password_switch)).check(matches(not(isDisplayed()))) - onView(ViewMatchers.withId(R.id.share_process_change_name_switch)).check(matches(not(isDisplayed()))) - onView(ViewMatchers.withId(R.id.share_process_allow_resharing_checkbox)).check(matches(isDisplayed())) - - // read-only - userShare.permissions = 17 // from server - onView(ViewMatchers.withId(R.id.share_process_permission_read_only)).check(matches(isChecked())) - onView(ViewMatchers.withId(R.id.share_process_permission_upload_editing)).check(matches(isNotChecked())) - goBack() - - // editing - userShare.permissions = MAXIMUM_PERMISSIONS_FOR_FILE // from server - openAdvancedPermissions(sut, userShare) - onView(ViewMatchers.withId(R.id.share_process_permission_read_only)).check(matches(isNotChecked())) - onView(ViewMatchers.withId(R.id.share_process_permission_upload_editing)).check(matches(isChecked())) - goBack() - - // allow reshare - userShare.permissions = 1 // from server - openAdvancedPermissions(sut, userShare) - onView(ViewMatchers.withId(R.id.share_process_allow_resharing_checkbox)).check(matches(isNotChecked())) - goBack() - - userShare.permissions = 17 // from server - openAdvancedPermissions(sut, userShare) - onView(ViewMatchers.withId(R.id.share_process_allow_resharing_checkbox)).check(matches(isChecked())) - goBack() - - // set expiration date - userShare.expirationDate = 1582019340000 - openAdvancedPermissions(sut, userShare) - onView(ViewMatchers.withId(R.id.share_process_set_exp_date_switch)).check(matches(isChecked())) - onView(ViewMatchers.withId(R.id.share_process_select_exp_date)).check(matches(not(withText("")))) - goBack() - - userShare.expirationDate = 0 - openAdvancedPermissions(sut, userShare) - onView(ViewMatchers.withId(R.id.share_process_set_exp_date_switch)).check(matches(isNotChecked())) - onView(ViewMatchers.withId(R.id.share_process_select_exp_date)).check(matches(withText(""))) } private fun suppressFDFAccessibilityChecks() { @@ -564,19 +682,28 @@ class FileDetailSharingFragmentIT : AbstractIT() { @Test @Suppress("MagicNumber") fun userOptionMenuFileSendNewEmail() { - val sut = FileDetailSharingFragment.newInstance(file, user) - activity.addFragment(sut) - setupSecondaryFragment() - shortSleep() - sut.refreshCapabilitiesFromDB() + launchActivity().use { scenario -> + scenario.onActivity { activity -> + val sut = FileDetailSharingFragment.newInstance(file, user) + activity.addFragment(sut) - val userShare = OCShare().apply { - isFolder = false - shareType = ShareType.USER - permissions = 17 + onIdleSync { + EspressoIdlingResource.increment() + setupSecondaryFragment() + sut.refreshCapabilitiesFromDB() + EspressoIdlingResource.decrement() + + val userShare = OCShare().apply { + remoteId = 1001L + isFolder = false + shareType = ShareType.USER + permissions = 17 + } + + verifySendNewEmail(sut, userShare) + } + } } - - verifySendNewEmail(sut, userShare) } // also applies for @@ -588,112 +715,118 @@ class FileDetailSharingFragmentIT : AbstractIT() { @Test @Suppress("MagicNumber") fun userOptionMenuFolderAdvancePermission() { - val sut = FileDetailSharingFragment.newInstance(file, user) - activity.addFragment(sut) - setupSecondaryFragment() - suppressFDFAccessibilityChecks() - shortSleep() - sut.refreshCapabilitiesFromDB() + launchActivity().use { scenario -> + scenario.onActivity { activity -> + val sut = FileDetailSharingFragment.newInstance(file, user) + activity.addFragment(sut) - val userShare = OCShare().apply { - isFolder = true - shareType = ShareType.USER - permissions = 17 + onIdleSync { + EspressoIdlingResource.increment() + setupSecondaryFragment() + suppressFDFAccessibilityChecks() + sut.refreshCapabilitiesFromDB() + EspressoIdlingResource.decrement() + + val userShare = OCShare().apply { + isFolder = true + shareType = ShareType.USER + permissions = 17 + } + + activity.runOnUiThread { sut.showSharingMenuActionSheet(userShare) } + + // check if items are visible + onView(ViewMatchers.withId(R.id.menu_share_advanced_permissions)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.menu_share_send_new_email)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.menu_share_send_link)).check(matches(not(isDisplayed()))) + onView(ViewMatchers.withId(R.id.menu_share_unshare)).check(matches(isDisplayed())) + + // click event + onView(ViewMatchers.withId(R.id.menu_share_advanced_permissions)).perform(ViewActions.click()) + + // validate view shown on screen + onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.file_request_radio_button)).check(matches(isDisplayed())) + onView( + ViewMatchers.withId(R.id.share_process_hide_download_checkbox) + ).check(matches(not(isDisplayed()))) + onView( + ViewMatchers.withId(R.id.share_process_set_password_switch) + ).check(matches(not(isDisplayed()))) + onView( + ViewMatchers.withId(R.id.share_process_change_name_switch) + ).check(matches(not(isDisplayed()))) + + // read-only + userShare.permissions = 17 // from server + onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isChecked())) + onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isNotChecked())) + onView(ViewMatchers.withId(R.id.file_request_radio_button)).check(matches(isNotChecked())) + goBack() + + // allow upload & editing + userShare.permissions = MAXIMUM_PERMISSIONS_FOR_FOLDER // from server + openAdvancedPermissions(sut, userShare) + onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isNotChecked())) + onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isChecked())) + onView(ViewMatchers.withId(R.id.file_request_radio_button)).check(matches(isNotChecked())) + goBack() + + // file request + userShare.permissions = 4 + openAdvancedPermissions(sut, userShare) + onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isNotChecked())) + onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isNotChecked())) + onView(ViewMatchers.withId(R.id.file_request_radio_button)).check(matches(isChecked())) + goBack() + + // set expiration date + userShare.expirationDate = 1582019340000 + openAdvancedPermissions(sut, userShare) + onView(ViewMatchers.withId(R.id.share_process_set_exp_date_switch)).check(matches(isChecked())) + onView(ViewMatchers.withId(R.id.share_process_select_exp_date)).check(matches(not(withText("")))) + goBack() + + userShare.expirationDate = 0 + openAdvancedPermissions(sut, userShare) + onView(ViewMatchers.withId(R.id.share_process_set_exp_date_switch)).check(matches(isNotChecked())) + onView(ViewMatchers.withId(R.id.share_process_select_exp_date)).check(matches(withText(""))) + } + } } - - activity.runOnUiThread { sut.showSharingMenuActionSheet(userShare) } - shortSleep() - waitForIdleSync() - - // check if items are visible - onView(ViewMatchers.withId(R.id.menu_share_advanced_permissions)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.menu_share_send_new_email)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.menu_share_send_link)).check(matches(not(isDisplayed()))) - onView(ViewMatchers.withId(R.id.menu_share_unshare)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.menu_share_add_another_link)).check(matches(not(isDisplayed()))) - - // click event - onView(ViewMatchers.withId(R.id.menu_share_advanced_permissions)).perform(ViewActions.click()) - - // validate view shown on screen - onView(ViewMatchers.withId(R.id.share_process_permission_read_only)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.share_process_permission_upload_editing)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.share_process_permission_file_drop)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.share_process_hide_download_checkbox)).check(matches(not(isDisplayed()))) - onView(ViewMatchers.withId(R.id.share_process_set_password_switch)).check(matches(not(isDisplayed()))) - onView(ViewMatchers.withId(R.id.share_process_change_name_switch)).check(matches(not(isDisplayed()))) - onView(ViewMatchers.withId(R.id.share_process_allow_resharing_checkbox)).check(matches(isDisplayed())) - - // read-only - userShare.permissions = 17 // from server - onView(ViewMatchers.withId(R.id.share_process_permission_read_only)).check(matches(isChecked())) - onView(ViewMatchers.withId(R.id.share_process_permission_upload_editing)).check(matches(isNotChecked())) - onView(ViewMatchers.withId(R.id.share_process_permission_file_drop)).check(matches(isNotChecked())) - goBack() - - // allow upload & editing - userShare.permissions = MAXIMUM_PERMISSIONS_FOR_FOLDER // from server - openAdvancedPermissions(sut, userShare) - onView(ViewMatchers.withId(R.id.share_process_permission_read_only)).check(matches(isNotChecked())) - onView(ViewMatchers.withId(R.id.share_process_permission_upload_editing)).check(matches(isChecked())) - onView(ViewMatchers.withId(R.id.share_process_permission_file_drop)).check(matches(isNotChecked())) - goBack() - - // file drop - userShare.permissions = 4 - openAdvancedPermissions(sut, userShare) - onView(ViewMatchers.withId(R.id.share_process_permission_read_only)).check(matches(isNotChecked())) - onView(ViewMatchers.withId(R.id.share_process_permission_upload_editing)).check(matches(isNotChecked())) - onView(ViewMatchers.withId(R.id.share_process_permission_file_drop)).check(matches(isChecked())) - goBack() - - // allow reshare - userShare.permissions = 1 // from server - openAdvancedPermissions(sut, userShare) - onView(ViewMatchers.withId(R.id.share_process_allow_resharing_checkbox)).check(matches(isNotChecked())) - goBack() - - userShare.permissions = 17 // from server - openAdvancedPermissions(sut, userShare) - onView(ViewMatchers.withId(R.id.share_process_allow_resharing_checkbox)).check(matches(isChecked())) - goBack() - - // set expiration date - userShare.expirationDate = 1582019340000 - openAdvancedPermissions(sut, userShare) - onView(ViewMatchers.withId(R.id.share_process_set_exp_date_switch)).check(matches(isChecked())) - onView(ViewMatchers.withId(R.id.share_process_select_exp_date)).check(matches(not(withText("")))) - goBack() - - userShare.expirationDate = 0 - openAdvancedPermissions(sut, userShare) - onView(ViewMatchers.withId(R.id.share_process_set_exp_date_switch)).check(matches(isNotChecked())) - onView(ViewMatchers.withId(R.id.share_process_select_exp_date)).check(matches(withText(""))) } // open bottom sheet with actions - private fun openAdvancedPermissions( - sut: FileDetailSharingFragment, - userShare: OCShare - ) { - activity.handler.post { - sut.showSharingMenuActionSheet(userShare) + private fun openAdvancedPermissions(sut: FileDetailSharingFragment, userShare: OCShare) { + launchActivity().use { scenario -> + scenario.onActivity { activity -> + onIdleSync { + EspressoIdlingResource.increment() + activity.handler.post { + sut.showSharingMenuActionSheet(userShare) + } + EspressoIdlingResource.decrement() + onView(ViewMatchers.withId(R.id.menu_share_advanced_permissions)).perform(ViewActions.click()) + } + } } - shortSleep() - waitForIdleSync() - onView(ViewMatchers.withId(R.id.menu_share_advanced_permissions)).perform(ViewActions.click()) } // remove the fragment shown private fun goBack() { - activity.handler.post { - val processFragment = - activity.supportFragmentManager.findFragmentByTag(FileDetailsSharingProcessFragment.TAG) as - FileDetailsSharingProcessFragment - processFragment.onBackPressed() + launchActivity().use { scenario -> + scenario.onActivity { activity -> + onIdleSync { + activity.handler.post { + val processFragment = + activity.supportFragmentManager.findFragmentByTag(FileDetailsSharingProcessFragment.TAG) as + FileDetailsSharingProcessFragment + processFragment.activity?.onBackPressedDispatcher?.onBackPressed() + } + } + } } - shortSleep() - waitForIdleSync() } // also applies for @@ -705,126 +838,105 @@ class FileDetailSharingFragmentIT : AbstractIT() { @Test @Suppress("MagicNumber") fun userOptionMenuFolderSendNewEmail() { - val sut = FileDetailSharingFragment.newInstance(file, user) - activity.addFragment(sut) - setupSecondaryFragment() - shortSleep() - sut.refreshCapabilitiesFromDB() + launchActivity().use { scenario -> + scenario.onActivity { activity -> + val sut = FileDetailSharingFragment.newInstance(file, user) + activity.addFragment(sut) + onIdleSync { + EspressoIdlingResource.increment() + setupSecondaryFragment() + sut.refreshCapabilitiesFromDB() + EspressoIdlingResource.decrement() - val userShare = OCShare().apply { - isFolder = true - shareType = ShareType.USER - permissions = 17 + val userShare = OCShare().apply { + isFolder = true + shareType = ShareType.USER + permissions = 17 + } + + verifySendNewEmail(sut, userShare) + } + } } - - verifySendNewEmail(sut, userShare) } /** * verify send new email note text */ - private fun verifySendNewEmail( - sut: FileDetailSharingFragment, - userShare: OCShare - ) { - activity.runOnUiThread { sut.showSharingMenuActionSheet(userShare) } + private fun verifySendNewEmail(sut: FileDetailSharingFragment, userShare: OCShare) { + launchActivity().use { scenario -> + scenario.onActivity { activity -> + onIdleSync { + EspressoIdlingResource.increment() + activity.runOnUiThread { sut.showSharingMenuActionSheet(userShare) } + EspressoIdlingResource.decrement() - waitForIdleSync() - // click event - onView(ViewMatchers.withId(R.id.menu_share_send_new_email)).perform(ViewActions.click()) + // click event + onView(ViewMatchers.withId(R.id.menu_share_send_new_email)).perform(ViewActions.click()) - // validate view shown on screen - onView(ViewMatchers.withId(R.id.note_text)).check(matches(isDisplayed())) + // validate view shown on screen + onView(ViewMatchers.withId(R.id.note_text)).check(matches(isDisplayed())) + } + } + } } @Test fun testUploadAndEditingSharePermissions() { - val share = OCShare().apply { - permissions = MAXIMUM_PERMISSIONS_FOR_FOLDER + val testCases = mapOf( + MAXIMUM_PERMISSIONS_FOR_FOLDER to true, + NO_PERMISSION to false, + READ_PERMISSION_FLAG to false, + CREATE_PERMISSION_FLAG to false, + DELETE_PERMISSION_FLAG to false, + SHARE_PERMISSION_FLAG to false + ) + + val share = OCShare() + for ((permission, expected) in testCases) { + share.permissions = permission + assertEquals("Failed for permission: $permission", expected, SharePermissionManager.canEdit(share)) } - assertTrue(SharingMenuHelper.isUploadAndEditingAllowed(share)) - - share.permissions = NO_PERMISSION - assertFalse(SharingMenuHelper.isUploadAndEditingAllowed(share)) - - share.permissions = READ_PERMISSION_FLAG - assertFalse(SharingMenuHelper.isUploadAndEditingAllowed(share)) - - share.permissions = CREATE_PERMISSION_FLAG - assertFalse(SharingMenuHelper.isUploadAndEditingAllowed(share)) - - share.permissions = DELETE_PERMISSION_FLAG - assertFalse(SharingMenuHelper.isUploadAndEditingAllowed(share)) - - share.permissions = SHARE_PERMISSION_FLAG - assertFalse(SharingMenuHelper.isUploadAndEditingAllowed(share)) } @Test - @Suppress("MagicNumber") fun testReadOnlySharePermissions() { - val share = OCShare().apply { - permissions = 17 + val testCases = mapOf( + READ_PERMISSION_FLAG to true, + NO_PERMISSION to false, + CREATE_PERMISSION_FLAG to false, + DELETE_PERMISSION_FLAG to false, + SHARE_PERMISSION_FLAG to false, + MAXIMUM_PERMISSIONS_FOR_FOLDER to false, + MAXIMUM_PERMISSIONS_FOR_FILE to false + ) + + val share = OCShare() + for ((permission, expected) in testCases) { + share.permissions = permission + assertEquals("Failed for permission: $permission", expected, SharePermissionManager.isViewOnly(share)) } - assertTrue(SharingMenuHelper.isReadOnly(share)) - - share.permissions = NO_PERMISSION - assertFalse(SharingMenuHelper.isReadOnly(share)) - - share.permissions = READ_PERMISSION_FLAG - assertTrue(SharingMenuHelper.isReadOnly(share)) - - share.permissions = CREATE_PERMISSION_FLAG - assertFalse(SharingMenuHelper.isReadOnly(share)) - - share.permissions = DELETE_PERMISSION_FLAG - assertFalse(SharingMenuHelper.isReadOnly(share)) - - share.permissions = SHARE_PERMISSION_FLAG - assertFalse(SharingMenuHelper.isReadOnly(share)) - - share.permissions = MAXIMUM_PERMISSIONS_FOR_FOLDER - assertFalse(SharingMenuHelper.isReadOnly(share)) - - share.permissions = MAXIMUM_PERMISSIONS_FOR_FILE - assertFalse(SharingMenuHelper.isReadOnly(share)) } @Test - @Suppress("MagicNumber") - fun testFileDropSharePermissions() { + fun testFileRequestSharePermission() { + val testCases = mapOf( + CREATE_PERMISSION_FLAG to true, + NO_PERMISSION to false, + READ_PERMISSION_FLAG to false, + DELETE_PERMISSION_FLAG to false, + SHARE_PERMISSION_FLAG to false, + MAXIMUM_PERMISSIONS_FOR_FOLDER to false, + MAXIMUM_PERMISSIONS_FOR_FILE to false + ) + val share = OCShare().apply { - permissions = 4 + isFolder = true } - assertTrue(SharingMenuHelper.isFileDrop(share)) - share.permissions = NO_PERMISSION - assertFalse(SharingMenuHelper.isFileDrop(share)) - - share.permissions = READ_PERMISSION_FLAG - assertFalse(SharingMenuHelper.isFileDrop(share)) - - share.permissions = CREATE_PERMISSION_FLAG - assertTrue(SharingMenuHelper.isFileDrop(share)) - - share.permissions = DELETE_PERMISSION_FLAG - assertFalse(SharingMenuHelper.isFileDrop(share)) - - share.permissions = SHARE_PERMISSION_FLAG - assertFalse(SharingMenuHelper.isFileDrop(share)) - - share.permissions = MAXIMUM_PERMISSIONS_FOR_FOLDER - assertFalse(SharingMenuHelper.isFileDrop(share)) - - share.permissions = MAXIMUM_PERMISSIONS_FOR_FILE - assertFalse(SharingMenuHelper.isFileDrop(share)) - } - - @After - override fun after() { - activity.storageManager.cleanShares() - activity.finish() - - super.after() + for ((permission, expected) in testCases) { + share.permissions = permission + assertEquals("Failed for permission: $permission", expected, SharePermissionManager.isFileRequest(share)) + } } } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/GalleryFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/GalleryFragmentIT.kt index 4992af8..9712fe4 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/GalleryFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/GalleryFragmentIT.kt @@ -1,9 +1,10 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.fragment @@ -11,69 +12,89 @@ import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint -import androidx.test.espresso.intent.rule.IntentsTestRule +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot import com.nextcloud.test.TestActivity import com.owncloud.android.AbstractIT import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.ThumbnailsCacheManager -import com.owncloud.android.datamodel.ThumbnailsCacheManager.InitDiskCacheTask import com.owncloud.android.datamodel.ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.files.model.ImageDimension +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.ScreenshotTest import org.junit.After import org.junit.Assert.assertNotNull import org.junit.Before -import org.junit.Rule import org.junit.Test import java.util.Random class GalleryFragmentIT : AbstractIT() { - @get:Rule - val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false) - - lateinit var activity: TestActivity - val random = Random(1) + private val testClassName = "com.owncloud.android.ui.fragment.GalleryFragmentIT" + private val random = Random(1) @Before - fun before() { - activity = testActivityRule.launchActivity(null) + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) // initialise thumbnails cache on background thread - InitDiskCacheTask().execute() + @Suppress("DEPRECATION") + ThumbnailsCacheManager.initDiskCacheAsync() } @After - override fun after() { + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) ThumbnailsCacheManager.clearCache() - - super.after() } + @Test + @UiThread @ScreenshotTest - @Test fun showEmpty() { - val sut = GalleryFragment() - activity.addFragment(sut) + launchActivity().use { scenario -> + scenario.onActivity { activity -> + onIdleSync { + EspressoIdlingResource.increment() + val sut = GalleryFragment() + activity.addFragment(sut) + EspressoIdlingResource.decrement() - waitForIdleSync() - - screenshot(activity) + val screenShotName = createName(testClassName + "_" + "showEmpty", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(activity, screenShotName) + } + } + } } @Test + @UiThread @ScreenshotTest fun showGallery() { - createImage(10000001, 700, 300) - createImage(10000002, 500, 300) - createImage(10000007, 300, 400) + launchActivity().use { scenario -> + scenario.onActivity { activity -> + onIdleSync { + EspressoIdlingResource.increment() + createImage(10000001, 700, 300) + createImage(10000002, 500, 300) + createImage(10000007, 300, 400) - val sut = GalleryFragment() - activity.addFragment(sut) + val sut = GalleryFragment() + activity.addFragment(sut) + EspressoIdlingResource.decrement() - waitForIdleSync() - shortSleep() - screenshot(activity) + val screenShotName = createName(testClassName + "_" + "showGallery", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(activity, screenShotName) + } + } + } } private fun createImage(id: Int, width: Int? = null, height: Int? = null) { diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/GroupfolderListFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/GroupfolderListFragmentIT.kt index 30177b1..ba41967 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/GroupfolderListFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/GroupfolderListFragmentIT.kt @@ -1,74 +1,98 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.fragment -import androidx.test.espresso.intent.rule.IntentsTestRule +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot import com.nextcloud.android.lib.resources.groupfolders.Groupfolder import com.nextcloud.test.TestActivity import com.owncloud.android.AbstractIT +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.ScreenshotTest +import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test class GroupfolderListFragmentIT : AbstractIT() { - @get:Rule - val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false) - - lateinit var activity: TestActivity + private val testClassName = "com.owncloud.android.ui.fragment.GroupfolderListFragmentIT" @Before - fun before() { - activity = testActivityRule.launchActivity(null) + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) } @Test + @UiThread @ScreenshotTest fun showGroupfolder() { - val sut = GroupfolderListFragment() - activity.addFragment(sut) + launchActivity().use { scenario -> + scenario.onActivity { activity -> + onIdleSync { + EspressoIdlingResource.increment() - shortSleep() // to let async task finish + val sut = GroupfolderListFragment() + activity.addFragment(sut) - activity.runOnUiThread { - sut.setAdapter(null) - sut.setData( - mapOf( - Pair("2", Groupfolder(2, "/subfolder/group")) - ) - ) + sut.setAdapter(null) + sut.setData( + mapOf( + Pair("2", Groupfolder(2, "/subfolder/group")) + ) + ) + + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "showGroupfolder", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(activity, screenShotName) + } + } } - - waitForIdleSync() - shortSleep() - screenshot(activity) } @Test + @UiThread @ScreenshotTest fun showGroupfolders() { - val sut = GroupfolderListFragment() - activity.addFragment(sut) + launchActivity().use { scenario -> + scenario.onActivity { activity -> + onIdleSync { + EspressoIdlingResource.increment() - shortSleep() // to let async task finish + val sut = GroupfolderListFragment() + activity.addFragment(sut) - activity.runOnUiThread { - sut.setAdapter(null) - sut.setData( - mapOf( - Pair("1", Groupfolder(1, "/test/")), - Pair("2", Groupfolder(2, "/subfolder/group")) - ) - ) + sut.setAdapter(null) + sut.setData( + mapOf( + Pair("1", Groupfolder(1, "/test/")), + Pair("2", Groupfolder(2, "/subfolder/group")) + ) + ) + + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "showGroupfolders", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(activity, screenShotName) + } + } } - - waitForIdleSync() - shortSleep() - screenshot(activity) } } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt index c11a315..793a6bc 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt @@ -1,364 +1,437 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.fragment -import androidx.test.espresso.intent.rule.IntentsTestRule -import com.nextcloud.test.GrantStoragePermissionRule +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import com.nextcloud.test.GrantStoragePermissionRule.Companion.grant import com.nextcloud.test.TestActivity import com.owncloud.android.AbstractIT import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.resources.shares.ShareType import com.owncloud.android.lib.resources.shares.ShareeUser +import com.owncloud.android.lib.resources.tags.Tag +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.MimeType import com.owncloud.android.utils.ScreenshotTest +import org.junit.After import org.junit.Assert +import org.junit.Before import org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule class OCFileListFragmentStaticServerIT : AbstractIT() { - @get:Rule - val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false) + private val testClassName = "com.owncloud.android.ui.fragment.OCFileListFragmentStaticServerIT" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } @get:Rule - val permissionRule = GrantStoragePermissionRule.grant() + var storagePermissionRule: TestRule = grant() @Test + @UiThread @ScreenshotTest @Suppress("MagicNumber") fun showFiles() { - val sut = testActivityRule.launchActivity(null) + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() - OCFile("/1.png").apply { - mimeType = "image/png" - fileLength = 1024000 - modificationTimestamp = 1188206955000 - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - sut.storageManager.saveFile(this) + OCFile("/1.png").apply { + remoteId = "00000001" + mimeType = "image/png" + fileLength = 1024000 + modificationTimestamp = 1188206955000 + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + sut.storageManager.saveFile(this) + } + + OCFile("/image.png").apply { + remoteId = "00000002" + mimeType = "image/png" + isPreviewAvailable = false + fileLength = 3072000 + modificationTimestamp = 746443755000 + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + tags = listOf(Tag("", "Top secret", null)) + sut.storageManager.saveFile(this) + } + + OCFile("/live photo.png").apply { + remoteId = "00000003" + mimeType = "image/png" + isPreviewAvailable = false + fileLength = 3072000 + modificationTimestamp = 746443755000 + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + setLivePhoto("/video.mov") + sut.storageManager.saveFile(this) + } + + OCFile("/video.mp4").apply { + remoteId = "00000004" + mimeType = "video/mp4" + isPreviewAvailable = false + fileLength = 12092000 + modificationTimestamp = 746143952000 + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + tags = listOf(Tag("", "Confidential", null), Tag("", "+5", null)) + sut.storageManager.saveFile(this) + } + + sut.addFragment(OCFileListFragment()) + + val fragment = (sut.fragment as OCFileListFragment) + val root = sut.storageManager.getFileByEncryptedRemotePath("/") + fragment.listDirectory(root, false, false) + + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "showFiles", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } } - - OCFile("/image.png").apply { - mimeType = "image/png" - isPreviewAvailable = false - fileLength = 3072000 - modificationTimestamp = 746443755000 - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - tags = listOf("Top secret") - sut.storageManager.saveFile(this) - } - - OCFile("/live photo.png").apply { - mimeType = "image/png" - isPreviewAvailable = false - fileLength = 3072000 - modificationTimestamp = 746443755000 - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - setLivePhoto("/video.mov") - sut.storageManager.saveFile(this) - } - - OCFile("/video.mp4").apply { - mimeType = "video/mp4" - isPreviewAvailable = false - fileLength = 12092000 - modificationTimestamp = 746143952000 - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - tags = listOf("Confidential", "+5") - sut.storageManager.saveFile(this) - } - - sut.addFragment(OCFileListFragment()) - - val fragment = (sut.fragment as OCFileListFragment) - val root = sut.storageManager.getFileByEncryptedRemotePath("/") - - shortSleep() - - sut.runOnUiThread { fragment.listDirectory(root, false, false) } - - waitForIdleSync() - - screenshot(sut) } /** * Use same values as {@link FileDetailSharingFragmentIT listSharesFileAllShareTypes } */ @Test + @UiThread @ScreenshotTest fun showSharedFiles() { - val sut = testActivityRule.launchActivity(null) - val fragment = OCFileListFragment() + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() - OCFile("/sharedToUser.jpg").apply { - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - isSharedWithSharee = true - sharees = listOf(ShareeUser("Admin", "Server Admin", ShareType.USER)) - modificationTimestamp = 1000 - sut.storageManager.saveFile(this) + val fragment = OCFileListFragment() + + OCFile("/sharedToUser.jpg").apply { + remoteId = "00000001" + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + isSharedWithSharee = true + sharees = listOf(ShareeUser("Admin", "Server Admin", ShareType.USER)) + modificationTimestamp = 1000 + sut.storageManager.saveFile(this) + } + + OCFile("/sharedToGroup.jpg").apply { + remoteId = "00000002" + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + isSharedWithSharee = true + sharees = listOf(ShareeUser("group", "Group", ShareType.GROUP)) + modificationTimestamp = 1000 + sut.storageManager.saveFile(this) + } + + OCFile("/sharedToEmail.jpg").apply { + remoteId = "00000003" + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + isSharedWithSharee = true + sharees = + listOf( + ShareeUser("admin@nextcloud.localhost", "admin@nextcloud.localhost", ShareType.EMAIL) + ) + modificationTimestamp = 1000 + sut.storageManager.saveFile(this) + } + + OCFile("/publicLink.jpg").apply { + remoteId = "00000004" + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + isSharedViaLink = true + modificationTimestamp = 1000 + sut.storageManager.saveFile(this) + } + + OCFile("/sharedToFederatedUser.jpg").apply { + remoteId = "00000005" + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + isSharedWithSharee = true + sharees = listOf( + ShareeUser( + "admin@remote.nextcloud.com", + "admin@remote.nextcloud.com (remote)", + ShareType.FEDERATED + ) + ) + modificationTimestamp = 1000 + sut.storageManager.saveFile(this) + } + + OCFile("/sharedToPersonalCircle.jpg").apply { + remoteId = "00000006" + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + isSharedWithSharee = true + sharees = listOf(ShareeUser("circle", "Circle (Personal circle)", ShareType.CIRCLE)) + modificationTimestamp = 1000 + sut.storageManager.saveFile(this) + } + + OCFile("/sharedToUserRoom.jpg").apply { + remoteId = "00000007" + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + isSharedWithSharee = true + sharees = listOf(ShareeUser("Conversation", "Admin", ShareType.ROOM)) + modificationTimestamp = 1000 + sut.storageManager.saveFile(this) + } + + OCFile("/sharedToGroupRoom.jpg").apply { + remoteId = "00000008" + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + isSharedWithSharee = true + sharees = listOf(ShareeUser("Conversation", "Meeting", ShareType.ROOM)) + modificationTimestamp = 1000 + sut.storageManager.saveFile(this) + } + + OCFile("/sharedToUsers.jpg").apply { + remoteId = "00000009" + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + isSharedWithSharee = true + sharees = listOf( + ShareeUser("Admin", "Server Admin", ShareType.USER), + ShareeUser("User", "User", ShareType.USER), + ShareeUser("Christine", "Christine Scott", ShareType.USER) + ) + modificationTimestamp = 1000 + sut.storageManager.saveFile(this) + } + + OCFile("/notShared.jpg").apply { + remoteId = "000000010" + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + modificationTimestamp = 1000 + sut.storageManager.saveFile(this) + } + + OCFile("/Foo%e2%80%aedm.exe").apply { + remoteId = "000000011" + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + modificationTimestamp = 1000 + sut.storageManager.saveFile(this) + } + + sut.addFragment(fragment) + val root = sut.storageManager.getFileByEncryptedRemotePath("/") + fragment.listDirectory(root, false, false) + fragment.adapter.setShowShareAvatar(true) + + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "showSharedFiles", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } } - - OCFile("/sharedToGroup.jpg").apply { - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - isSharedWithSharee = true - sharees = listOf(ShareeUser("group", "Group", ShareType.GROUP)) - modificationTimestamp = 1000 - sut.storageManager.saveFile(this) - } - - OCFile("/sharedToEmail.jpg").apply { - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - isSharedWithSharee = true - sharees = listOf(ShareeUser("admin@nextcloud.localhost", "admin@nextcloud.localhost", ShareType.EMAIL)) - modificationTimestamp = 1000 - sut.storageManager.saveFile(this) - } - - OCFile("/publicLink.jpg").apply { - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - isSharedViaLink = true - modificationTimestamp = 1000 - sut.storageManager.saveFile(this) - } - - OCFile("/sharedToFederatedUser.jpg").apply { - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - isSharedWithSharee = true - sharees = listOf( - ShareeUser("admin@remote.nextcloud.com", "admin@remote.nextcloud.com (remote)", ShareType.FEDERATED) - ) - modificationTimestamp = 1000 - sut.storageManager.saveFile(this) - } - - OCFile("/sharedToPersonalCircle.jpg").apply { - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - isSharedWithSharee = true - sharees = listOf(ShareeUser("circle", "Circle (Personal circle)", ShareType.CIRCLE)) - modificationTimestamp = 1000 - sut.storageManager.saveFile(this) - } - - // as we cannot distinguish circle types, we do not need them right now -// OCFile("/sharedToPublicCircle.jpg").apply { -// parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId -// isSharedWithSharee = true -// sharees = listOf(ShareeUser("circle", "Circle (Public circle)", ShareType.CIRCLE)) -// modificationTimestamp = 1000 -// sut.storageManager.saveFile(this) -// } -// -// OCFile("/sharedToClosedCircle.jpg").apply { -// parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId -// isSharedWithSharee = true -// sharees = listOf(ShareeUser("circle", "Circle (Closed circle)", ShareType.CIRCLE)) -// modificationTimestamp = 1000 -// sut.storageManager.saveFile(this) -// } -// -// OCFile("/sharedToSecretCircle.jpg").apply { -// parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId -// isSharedWithSharee = true -// sharees = listOf(ShareeUser("circle", "Circle (Secret circle)", ShareType.CIRCLE)) -// modificationTimestamp = 1000 -// sut.storageManager.saveFile(this) -// } - - OCFile("/sharedToUserRoom.jpg").apply { - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - isSharedWithSharee = true - sharees = listOf(ShareeUser("Conversation", "Admin", ShareType.ROOM)) - modificationTimestamp = 1000 - sut.storageManager.saveFile(this) - } - - OCFile("/sharedToGroupRoom.jpg").apply { - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - isSharedWithSharee = true - sharees = listOf(ShareeUser("Conversation", "Meeting", ShareType.ROOM)) - modificationTimestamp = 1000 - sut.storageManager.saveFile(this) - } - - OCFile("/sharedToUsers.jpg").apply { - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - isSharedWithSharee = true - sharees = listOf( - ShareeUser("Admin", "Server Admin", ShareType.USER), - ShareeUser("User", "User", ShareType.USER), - ShareeUser("Christine", "Christine Scott", ShareType.USER) - ) - modificationTimestamp = 1000 - sut.storageManager.saveFile(this) - } - - OCFile("/notShared.jpg").apply { - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - modificationTimestamp = 1000 - sut.storageManager.saveFile(this) - } - - sut.addFragment(fragment) - - shortSleep() - - val root = sut.storageManager.getFileByEncryptedRemotePath("/") - - sut.runOnUiThread { - fragment.listDirectory(root, false, false) - fragment.adapter.setShowShareAvatar(true) - } - - waitForIdleSync() - shortSleep() - shortSleep() - shortSleep() - - screenshot(sut) } /** * Use same values as {@link FileDetailSharingFragmentIT listSharesFileAllShareTypes } */ @Test + @UiThread @ScreenshotTest fun showFolderTypes() { - val sut = testActivityRule.launchActivity(null) - val fragment = OCFileListFragment() + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() - OCFile("/normal/").apply { - mimeType = MimeType.DIRECTORY - modificationTimestamp = 1624003571000 - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - sut.storageManager.saveFile(this) + val fragment = OCFileListFragment() + + OCFile("/normal/").apply { + remoteId = "00000001" + mimeType = MimeType.DIRECTORY + modificationTimestamp = 1624003571000 + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + sut.storageManager.saveFile(this) + } + + OCFile("/sharedViaLink/").apply { + remoteId = "00000002" + mimeType = MimeType.DIRECTORY + isSharedViaLink = true + modificationTimestamp = 1619003571000 + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + sut.storageManager.saveFile(this) + } + + OCFile("/share/").apply { + remoteId = "00000003" + mimeType = MimeType.DIRECTORY + isSharedWithSharee = true + modificationTimestamp = 1619303571000 + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + sut.storageManager.saveFile(this) + } + + OCFile("/groupFolder/").apply { + remoteId = "00000004" + mimeType = MimeType.DIRECTORY + modificationTimestamp = 1615003571000 + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + permissions += "M" + sut.storageManager.saveFile(this) + } + + OCFile("/encrypted/").apply { + remoteId = "00000005" + mimeType = MimeType.DIRECTORY + isEncrypted = true + decryptedRemotePath = "/encrypted/" + modificationTimestamp = 1614003571000 + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + sut.storageManager.saveFile(this) + } + + OCFile("/locked/").apply { + remoteId = "00000006" + mimeType = MimeType.DIRECTORY + isLocked = true + decryptedRemotePath = "/locked/" + modificationTimestamp = 1613003571000 + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + sut.storageManager.saveFile(this) + } + + OCFile("/offlineOperation/").apply { + mimeType = MimeType.DIRECTORY + decryptedRemotePath = "/offlineOperation/" + modificationTimestamp = System.currentTimeMillis() + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + sut.storageManager.saveFile(this) + } + + sut.addFragment(fragment) + + val root = sut.storageManager.getFileByEncryptedRemotePath("/") + fragment.listDirectory(root, false, false) + fragment.adapter.setShowShareAvatar(true) + + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "showFolderTypes", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } } - - OCFile("/sharedViaLink/").apply { - mimeType = MimeType.DIRECTORY - isSharedViaLink = true - modificationTimestamp = 1619003571000 - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - sut.storageManager.saveFile(this) - } - - OCFile("/share/").apply { - mimeType = MimeType.DIRECTORY - isSharedWithSharee = true - modificationTimestamp = 1619303571000 - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - sut.storageManager.saveFile(this) - } - - OCFile("/groupFolder/").apply { - mimeType = MimeType.DIRECTORY - modificationTimestamp = 1615003571000 - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - permissions += "M" - sut.storageManager.saveFile(this) - } - - OCFile("/encrypted/").apply { - mimeType = MimeType.DIRECTORY - isEncrypted = true - decryptedRemotePath = "/encrypted/" - modificationTimestamp = 1614003571000 - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - sut.storageManager.saveFile(this) - } - - OCFile("/locked/").apply { - mimeType = MimeType.DIRECTORY - isLocked = true - decryptedRemotePath = "/locked/" - modificationTimestamp = 1613003571000 - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - sut.storageManager.saveFile(this) - } - - sut.addFragment(fragment) - - shortSleep() - - val root = sut.storageManager.getFileByEncryptedRemotePath("/") - - sut.runOnUiThread { - fragment.listDirectory(root, false, false) - fragment.adapter.setShowShareAvatar(true) - } - - waitForIdleSync() - shortSleep() - shortSleep() - shortSleep() - - screenshot(sut) } @Test + @UiThread @ScreenshotTest @Suppress("MagicNumber") fun showRichWorkspace() { - val sut = testActivityRule.launchActivity(null) - val fragment = OCFileListFragment() + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() - val folder = OCFile("/test/") - folder.setFolder() - sut.storageManager.saveFile(folder) + val fragment = OCFileListFragment() - val imageFile = OCFile("/test/image.png") - imageFile.mimeType = "image/png" - imageFile.fileLength = 1024000 - imageFile.modificationTimestamp = 1188206955000 - imageFile.parentId = sut.storageManager.getFileByEncryptedRemotePath("/test/").fileId - imageFile.storagePath = getFile("java.md").absolutePath - sut.storageManager.saveFile(imageFile) + val folder = OCFile("/test/") + folder.setFolder() + sut.storageManager.saveFile(folder) - sut.addFragment(fragment) - val testFolder: OCFile = sut.storageManager.getFileByEncryptedRemotePath("/test/") - testFolder.richWorkspace = getFile("java.md").readText() + val imageFile = OCFile("/test/image.png").apply { + remoteId = "00000001" + mimeType = "image/png" + fileLength = 1024000 + modificationTimestamp = 1188206955000 + parentId = sut.storageManager.getFileByEncryptedRemotePath("/test/").fileId + storagePath = getFile("java.md").absolutePath + } - sut.runOnUiThread { fragment.listDirectory(testFolder, false, false) } + sut.storageManager.saveFile(imageFile) - shortSleep() + sut.addFragment(fragment) + val testFolder: OCFile = sut.storageManager.getFileByEncryptedRemotePath("/test/") + testFolder.richWorkspace = getFile("java.md").readText() + fragment.listDirectory(testFolder, false, false) - screenshot(sut) + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "showRichWorkspace", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } @Test + @UiThread fun shouldShowHeader() { - val activity = testActivityRule.launchActivity(null) - val sut = OCFileListFragment() + launchActivity().use { scenario -> + scenario.onActivity { activity -> + onIdleSync { + EspressoIdlingResource.increment() + val sut = OCFileListFragment() + val folder = OCFile("/test/").apply { + remoteId = "000001" + setFolder() + } + activity.storageManager.saveFile(folder) + activity.addFragment(sut) + val testFolder: OCFile = activity.storageManager.getFileByEncryptedRemotePath("/test/") + EspressoIdlingResource.decrement() - val folder = OCFile("/test/") - folder.setFolder() - activity.storageManager.saveFile(folder) + // richWorkspace is not set + Assert.assertFalse(sut.adapter.shouldShowHeader()) - activity.addFragment(sut) - val testFolder: OCFile = activity.storageManager.getFileByEncryptedRemotePath("/test/") + EspressoIdlingResource.increment() + testFolder.richWorkspace = " " + activity.storageManager.saveFile(testFolder) + sut.adapter.swapDirectory(user, testFolder, activity.storageManager, false, "") + EspressoIdlingResource.decrement() - activity.runOnUiThread { - // richWorkspace is not set - Assert.assertFalse(sut.adapter.shouldShowHeader()) + Assert.assertFalse(sut.adapter.shouldShowHeader()) - testFolder.richWorkspace = " " - activity.storageManager.saveFile(testFolder) - sut.adapter.swapDirectory(user, testFolder, activity.storageManager, false, "") - Assert.assertFalse(sut.adapter.shouldShowHeader()) + EspressoIdlingResource.increment() + testFolder.richWorkspace = null + activity.storageManager.saveFile(testFolder) + sut.adapter.swapDirectory(user, testFolder, activity.storageManager, false, "") + EspressoIdlingResource.decrement() + Assert.assertFalse(sut.adapter.shouldShowHeader()) - testFolder.richWorkspace = null - activity.storageManager.saveFile(testFolder) - sut.adapter.swapDirectory(user, testFolder, activity.storageManager, false, "") - Assert.assertFalse(sut.adapter.shouldShowHeader()) + EspressoIdlingResource.increment() + testFolder.richWorkspace = "1" + activity.storageManager.saveFile(testFolder) + sut.adapter.setCurrentDirectory(testFolder) + EspressoIdlingResource.decrement() - testFolder.richWorkspace = "1" - activity.storageManager.saveFile(testFolder) - sut.adapter.setCurrentDirectory(testFolder) - Assert.assertTrue(sut.adapter.shouldShowHeader()) + Assert.assertTrue(sut.adapter.shouldShowHeader()) + } + } } } } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt index 48bea8b..9eabce7 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt @@ -3,168 +3,183 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.fragment import android.view.View -import androidx.test.espresso.intent.rule.IntentsTestRule -import com.nextcloud.test.GrantStoragePermissionRule +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import com.nextcloud.test.GrantStoragePermissionRule.Companion.grant import com.nextcloud.test.TestActivity import com.owncloud.android.AbstractIT import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.resources.shares.OCShare import com.owncloud.android.lib.resources.shares.ShareType +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.ScreenshotTest +import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test +import org.junit.rules.TestRule internal class SharedListFragmentIT : AbstractIT() { - @get:Rule - val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false) - - @get:Rule - val permissionRule = GrantStoragePermissionRule.grant() - - lateinit var sut: TestActivity + private val testClassName = "com.owncloud.android.ui.fragment.SharedListFragmentIT" @Before - fun before() { - sut = testActivityRule.launchActivity(null) + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) } + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } + + @get:Rule + var storagePermissionRule: TestRule = grant() + @Test + @UiThread @ScreenshotTest fun showSharedFiles() { - val fragment = SharedListFragment() + launchActivity().use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + EspressoIdlingResource.increment() - val file = OCFile("/shared to admin.png").apply { - remoteId = "00000001" - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - mimeType = "image/png" - fileLength = 1024000 - modificationTimestamp = 1188206955 - permissions = OCFile.PERMISSION_CAN_RESHARE - sut.storageManager.saveFile(this) - } + val fragment = SharedListFragment() - val file1 = OCFile("/shared to group.png").apply { - remoteId = "00000001" - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - mimeType = "image/png" - fileLength = 1024000 - modificationTimestamp = 1188206955 - permissions = OCFile.PERMISSION_CAN_RESHARE - sut.storageManager.saveFile(this) - } + val file = OCFile("/shared to admin.png").apply { + remoteId = "00000001" + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + mimeType = "image/png" + fileLength = 1024000 + modificationTimestamp = 1188206955 + permissions = OCFile.PERMISSION_CAN_RESHARE + sut.storageManager.saveFile(this) + } - val file2 = OCFile("/shared via public link.png").apply { - remoteId = "00000001" - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - mimeType = "image/png" - fileLength = 1024000 - modificationTimestamp = 1188206955 - permissions = OCFile.PERMISSION_CAN_RESHARE - sut.storageManager.saveFile(this) - } + val file1 = OCFile("/shared to group.png").apply { + remoteId = "00000001" + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + mimeType = "image/png" + fileLength = 1024000 + modificationTimestamp = 1188206955 + permissions = OCFile.PERMISSION_CAN_RESHARE + sut.storageManager.saveFile(this) + } - val file3 = OCFile("/shared to personal circle.png").apply { - remoteId = "00000001" - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - mimeType = "image/png" - fileLength = 1024000 - modificationTimestamp = 1188206955 - permissions = OCFile.PERMISSION_CAN_RESHARE - sut.storageManager.saveFile(this) - } + val file2 = OCFile("/shared via public link.png").apply { + remoteId = "00000001" + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + mimeType = "image/png" + fileLength = 1024000 + modificationTimestamp = 1188206955 + permissions = OCFile.PERMISSION_CAN_RESHARE + sut.storageManager.saveFile(this) + } - val file4 = OCFile("/shared to talk.png").apply { - remoteId = "00000001" - parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId - mimeType = "image/png" - fileLength = 1024000 - modificationTimestamp = 1188206955 - permissions = OCFile.PERMISSION_CAN_RESHARE - sut.storageManager.saveFile(this) - } + val file3 = OCFile("/shared to personal circle.png").apply { + remoteId = "00000001" + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + mimeType = "image/png" + fileLength = 1024000 + modificationTimestamp = 1188206955 + permissions = OCFile.PERMISSION_CAN_RESHARE + sut.storageManager.saveFile(this) + } - val shares = listOf( - OCShare(file.decryptedRemotePath).apply { - remoteId = 1 - shareType = ShareType.USER - sharedWithDisplayName = "Admin" - permissions = OCShare.MAXIMUM_PERMISSIONS_FOR_FILE - userId = getUserId(user) - sharedDate = 1188206955 - mimetype = "image/png" - sut.storageManager.saveShare(this) - }, + val file4 = OCFile("/shared to talk.png").apply { + remoteId = "00000001" + parentId = sut.storageManager.getFileByEncryptedRemotePath("/").fileId + mimeType = "image/png" + fileLength = 1024000 + modificationTimestamp = 1188206955 + permissions = OCFile.PERMISSION_CAN_RESHARE + sut.storageManager.saveFile(this) + } - OCShare(file1.decryptedRemotePath).apply { - remoteId = 2 - shareType = ShareType.GROUP - sharedWithDisplayName = "Group" - permissions = OCShare.MAXIMUM_PERMISSIONS_FOR_FILE - userId = getUserId(user) - sharedDate = 1188206955 - mimetype = "image/png" - sut.storageManager.saveShare(this) - }, + val shares = listOf( + OCShare(file.decryptedRemotePath).apply { + remoteId = 1 + shareType = ShareType.USER + sharedWithDisplayName = "Admin" + permissions = OCShare.MAXIMUM_PERMISSIONS_FOR_FILE + userId = getUserId(user) + sharedDate = 1188206955 + mimetype = "image/png" + sut.storageManager.saveShare(this) + }, - OCShare(file2.decryptedRemotePath).apply { - remoteId = 3 - shareType = ShareType.PUBLIC_LINK - label = "Customer" - sharedDate = 1188206955 - mimetype = "image/png" - sut.storageManager.saveShare(this) - }, + OCShare(file1.decryptedRemotePath).apply { + remoteId = 2 + shareType = ShareType.GROUP + sharedWithDisplayName = "Group" + permissions = OCShare.MAXIMUM_PERMISSIONS_FOR_FILE + userId = getUserId(user) + sharedDate = 1188206955 + mimetype = "image/png" + sut.storageManager.saveShare(this) + }, - OCShare(file3.decryptedRemotePath).apply { - remoteId = 4 - shareType = ShareType.CIRCLE - sharedWithDisplayName = "Personal circle" - permissions = OCShare.SHARE_PERMISSION_FLAG - userId = getUserId(user) - sharedDate = 1188206955 - mimetype = "image/png" - sut.storageManager.saveShare(this) - }, + OCShare(file2.decryptedRemotePath).apply { + remoteId = 3 + shareType = ShareType.PUBLIC_LINK + label = "Customer" + sharedDate = 1188206955 + mimetype = "image/png" + sut.storageManager.saveShare(this) + }, - OCShare(file4.decryptedRemotePath).apply { - remoteId = 11 - shareType = ShareType.ROOM - sharedWithDisplayName = "Admin" - permissions = OCShare.SHARE_PERMISSION_FLAG - userId = getUserId(user) - sharedDate = 1188206955 - mimetype = "image/png" - sut.storageManager.saveShare(this) + OCShare(file3.decryptedRemotePath).apply { + remoteId = 4 + shareType = ShareType.CIRCLE + sharedWithDisplayName = "Personal circle" + permissions = OCShare.SHARE_PERMISSION_FLAG + userId = getUserId(user) + sharedDate = 1188206955 + mimetype = "image/png" + sut.storageManager.saveShare(this) + }, + + OCShare(file4.decryptedRemotePath).apply { + remoteId = 11 + shareType = ShareType.ROOM + sharedWithDisplayName = "Admin" + permissions = OCShare.SHARE_PERMISSION_FLAG + userId = getUserId(user) + sharedDate = 1188206955 + mimetype = "image/png" + sut.storageManager.saveShare(this) + } + ) + + sut.addFragment(fragment) + + fragment.isLoading = false + fragment.mEmptyListContainer?.visibility = View.GONE + fragment.adapter.setData( + shares, + SearchType.SHARED_FILTER, + storageManager, + null, + true + ) + + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "showSharedFiles", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } } - ) - - sut.addFragment(fragment) - - shortSleep() - - sut.runOnUiThread { - fragment.isLoading = false - fragment.mEmptyListContainer.visibility = View.GONE - fragment.adapter.setData( - shares, - SearchType.SHARED_FILTER, - storageManager, - null, - true - ) } - - waitForIdleSync() - shortSleep() - shortSleep() - shortSleep() - - screenshot(sut) } } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/UnifiedSearchFakeRepository.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/UnifiedSearchFakeRepository.kt index d5b3d00..6173181 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/UnifiedSearchFakeRepository.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/UnifiedSearchFakeRepository.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.fragment diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/UnifiedSearchFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/UnifiedSearchFragmentIT.kt index dce1f59..0fa3bc4 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/UnifiedSearchFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/UnifiedSearchFragmentIT.kt @@ -1,86 +1,109 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.fragment -import androidx.test.espresso.intent.rule.IntentsTestRule -import androidx.test.internal.runner.junit4.statement.UiThreadStatement +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot import com.nextcloud.test.TestActivity import com.owncloud.android.AbstractIT import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.common.SearchResultEntry import com.owncloud.android.ui.unifiedsearch.UnifiedSearchSection import com.owncloud.android.ui.unifiedsearch.UnifiedSearchViewModel -import org.junit.Rule +import com.owncloud.android.utils.EspressoIdlingResource +import org.junit.After +import org.junit.Before import org.junit.Test import java.io.File class UnifiedSearchFragmentIT : AbstractIT() { - @get:Rule - val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false) - @Test - fun showSearchResult() { - val activity = testActivityRule.launchActivity(null) - val sut = UnifiedSearchFragment.newInstance(null, null) + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } - activity.addFragment(sut) - - shortSleep() - - UiThreadStatement.runOnUiThread { - sut.onSearchResultChanged( - listOf( - UnifiedSearchSection( - providerID = "files", - name = "Files", - entries = listOf( - SearchResultEntry( - "thumbnailUrl", - "Test", - "in Files", - "http://localhost/nc/index.php/apps/files/?dir=/Files&scrollto=Test", - "icon", - false - ) - ), - hasMoreResults = false - ) - ) - ) - } - shortSleep() + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) } @Test + @UiThread + fun showSearchResult() { + launchActivity().use { scenario -> + scenario.onActivity { activity -> + onIdleSync { + EspressoIdlingResource.increment() + val sut = UnifiedSearchFragment.newInstance(null, null, "/") + activity.addFragment(sut) + + sut.onSearchResultChanged( + listOf( + UnifiedSearchSection( + providerID = "files", + name = "Files", + entries = listOf( + SearchResultEntry( + "thumbnailUrl", + "Test", + "in Files", + "http://localhost/nc/index.php/apps/files/?dir=/Files&scrollto=Test", + "icon", + false + ) + ), + hasMoreResults = false + ) + ) + ) + EspressoIdlingResource.decrement() + onView(isRoot()).check(matches(isDisplayed())) + } + } + } + } + + @Test + @UiThread fun search() { - val activity = testActivityRule.launchActivity(null) as TestActivity - val sut = UnifiedSearchFragment.newInstance(null, null) - val testViewModel = UnifiedSearchViewModel(activity.application) - testViewModel.setConnectivityService(activity.connectivityServiceMock) - val localRepository = UnifiedSearchFakeRepository() - testViewModel.setRepository(localRepository) + launchActivity().use { scenario -> + scenario.onActivity { activity -> + onIdleSync { + EspressoIdlingResource.increment() - val ocFile = OCFile("/folder/test1.txt").apply { - storagePath = "/sdcard/1.txt" - storageManager.saveFile(this) + val sut = UnifiedSearchFragment.newInstance(null, null, "/") + val testViewModel = UnifiedSearchViewModel(activity.application) + testViewModel.setConnectivityService(activity.connectivityServiceMock) + val localRepository = UnifiedSearchFakeRepository() + testViewModel.setRepository(localRepository) + val ocFile = OCFile("/folder/test1.txt").apply { + storagePath = "/sdcard/1.txt" + storageManager.saveFile(this) + } + + File(ocFile.storagePath).createNewFile() + activity.addFragment(sut) + + sut.setViewModel(testViewModel) + sut.vm.setQuery("test") + sut.vm.initialQuery() + + EspressoIdlingResource.decrement() + onView(isRoot()).check(matches(isDisplayed())) + } + } } - - File(ocFile.storagePath).createNewFile() - - activity.addFragment(sut) - - shortSleep() - - UiThreadStatement.runOnUiThread { - sut.setViewModel(testViewModel) - sut.vm.setQuery("test") - sut.vm.initialQuery() - } - shortSleep() } } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/helpers/FileOperationsHelperIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/helpers/FileOperationsHelperIT.kt index 40f96f1..bee5789 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/helpers/FileOperationsHelperIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/helpers/FileOperationsHelperIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.helpers diff --git a/app/src/androidTest/java/com/owncloud/android/ui/helpers/UriUploaderIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/helpers/UriUploaderIT.kt index 2ded4eb..bd776c1 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/helpers/UriUploaderIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/helpers/UriUploaderIT.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2022 Álvaro Brey - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.helpers diff --git a/app/src/androidTest/java/com/owncloud/android/ui/preview/PreviewBitmapScreenshotIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/preview/PreviewBitmapScreenshotIT.kt index 0587ddb..294c3aa 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/preview/PreviewBitmapScreenshotIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/preview/PreviewBitmapScreenshotIT.kt @@ -1,43 +1,63 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.preview import android.content.Intent -import androidx.test.espresso.intent.rule.IntentsTestRule +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot import com.owncloud.android.AbstractIT +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.ScreenshotTest -import org.junit.Rule +import org.junit.After +import org.junit.Before import org.junit.Test class PreviewBitmapScreenshotIT : AbstractIT() { + private val testClassName = "com.owncloud.android.ui.preview.PreviewBitmapScreenshotIT" companion object { private const val PNG_FILE_ASSET = "imageFile.png" } - @get:Rule - val testActivityRule = IntentsTestRule(PreviewBitmapActivity::class.java, true, false) + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } @Test + @UiThread @ScreenshotTest fun showBitmap() { val pngFile = getFile(PNG_FILE_ASSET) - - val activity = testActivityRule.launchActivity( - Intent().putExtra( - PreviewBitmapActivity.EXTRA_BITMAP_PATH, - pngFile.absolutePath - ) + val intent = Intent(targetContext, PreviewBitmapActivity::class.java).putExtra( + PreviewBitmapActivity.EXTRA_BITMAP_PATH, + pngFile.absolutePath ) - shortSleep() - waitForIdleSync() - - screenshot(activity) + launchActivity(intent).use { scenario -> + scenario.onActivity { sut -> + onIdleSync { + val screenShotName = createName(testClassName + "_" + "showBitmap", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } } } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/preview/PreviewImageFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/preview/PreviewImageFragmentIT.kt deleted file mode 100644 index d785f1c..0000000 --- a/app/src/androidTest/java/com/owncloud/android/ui/preview/PreviewImageFragmentIT.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.owncloud.android.ui.preview - -import androidx.test.espresso.intent.rule.IntentsTestRule -import com.nextcloud.test.TestActivity -import com.owncloud.android.AbstractIT -import org.junit.Rule - -class PreviewImageFragmentIT : AbstractIT() { - @get:Rule - val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false) - - // Disabled for now due to strange failing when using entire test suite - // Findings so far: - // PreviewImageFragmentIT runs fine when only running this - // running it in whole test suite fails - // manually tried to execute LoadBitmapTask, but this does not start "doInBackground", but only creates class - - // @Test - // @ScreenshotTest - // fun corruptImage() { - // val activity = testActivityRule.launchActivity(null) - // - // val ocFile = OCFile("/test.png") - // val sut = PreviewImageFragment.newInstance(ocFile, true, false) - // - // activity.addFragment(sut) - // - // while (!sut.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { - // shortSleep() - // } - // - // screenshot(activity) - // } - // - // @Test - // @ScreenshotTest - // fun validImage() { - // val activity = testActivityRule.launchActivity(null) - // - // val ocFile = OCFile("/test.png") - // ocFile.storagePath = getFile("imageFile.png").absolutePath - // - // val sut = PreviewImageFragment.newInstance(ocFile, true, false) - // - // activity.addFragment(sut) - // - // while (!sut.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { - // shortSleep() - // } - // - // screenshot(activity) - // } -} diff --git a/app/src/androidTest/java/com/owncloud/android/ui/preview/PreviewTextFileFragmentTest.java b/app/src/androidTest/java/com/owncloud/android/ui/preview/PreviewTextFileFragmentTest.java deleted file mode 100644 index 93acf38..0000000 --- a/app/src/androidTest/java/com/owncloud/android/ui/preview/PreviewTextFileFragmentTest.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.owncloud.android.ui.preview; - -import com.owncloud.android.AbstractIT; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.ui.activity.FileDisplayActivity; -import com.owncloud.android.utils.MimeTypeUtil; -import com.owncloud.android.utils.ScreenshotTest; - -import org.junit.Rule; -import org.junit.Test; - -import java.io.File; -import java.io.IOException; - -import androidx.test.espresso.intent.rule.IntentsTestRule; - -public class PreviewTextFileFragmentTest extends AbstractIT { - @Rule public IntentsTestRule activityRule = new IntentsTestRule<>(FileDisplayActivity.class, - true, - false); - - @Test - @ScreenshotTest - public void displaySimpleTextFile() throws IOException { - FileDisplayActivity sut = activityRule.launchActivity(null); - - shortSleep(); - - File file = getDummyFile("nonEmpty.txt"); - OCFile test = new OCFile("/text.md"); - test.setMimeType(MimeTypeUtil.MIMETYPE_TEXT_MARKDOWN); - test.setStoragePath(file.getAbsolutePath()); - sut.startTextPreview(test, false); - - shortSleep(); - - screenshot(sut); - } - - @Test - @ScreenshotTest - public void displayJavaSnippetFile() throws IOException { - FileDisplayActivity sut = activityRule.launchActivity(null); - - shortSleep(); - - File file = getFile("java.md"); - OCFile test = new OCFile("/java.md"); - test.setMimeType(MimeTypeUtil.MIMETYPE_TEXT_MARKDOWN); - test.setStoragePath(file.getAbsolutePath()); - sut.startTextPreview(test, false); - - shortSleep(); - - screenshot(sut); - } -} diff --git a/app/src/androidTest/java/com/owncloud/android/ui/preview/PreviewTextFileFragmentTest.kt b/app/src/androidTest/java/com/owncloud/android/ui/preview/PreviewTextFileFragmentTest.kt new file mode 100644 index 0000000..d9063fd --- /dev/null +++ b/app/src/androidTest/java/com/owncloud/android/ui/preview/PreviewTextFileFragmentTest.kt @@ -0,0 +1,89 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.preview + +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import com.owncloud.android.AbstractIT +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.utils.EspressoIdlingResource +import com.owncloud.android.utils.MimeTypeUtil +import com.owncloud.android.utils.ScreenshotTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.io.IOException + +class PreviewTextFileFragmentTest : AbstractIT() { + private val testClassName = "com.owncloud.android.ui.preview.PreviewTextFileFragmentTest" + + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } + + @Test + @ScreenshotTest + @UiThread + @Throws(IOException::class) + fun displaySimpleTextFile() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + val test = OCFile("/text.md").apply { + mimeType = MimeTypeUtil.MIMETYPE_TEXT_MARKDOWN + storagePath = getDummyFile("nonEmpty.txt").absolutePath + } + + onIdleSync { + EspressoIdlingResource.increment() + sut.startTextPreview(test, true) + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "displaySimpleTextFile", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } + } + + @Test + @ScreenshotTest + @UiThread + @Throws(IOException::class) + fun displayJavaSnippetFile() { + launchActivity().use { scenario -> + scenario.onActivity { sut -> + val test = OCFile("/java.md").apply { + mimeType = MimeTypeUtil.MIMETYPE_TEXT_MARKDOWN + storagePath = getFile("java.md").absolutePath + } + + onIdleSync { + EspressoIdlingResource.increment() + sut.startTextPreview(test, true) + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "displayJavaSnippetFile", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(sut, screenShotName) + } + } + } + } +} diff --git a/app/src/androidTest/java/com/owncloud/android/ui/preview/pdf/PreviewPdfFragmentScreenshotIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/preview/pdf/PreviewPdfFragmentScreenshotIT.kt index 052ba28..5c0d3c1 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/preview/pdf/PreviewPdfFragmentScreenshotIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/preview/pdf/PreviewPdfFragmentScreenshotIT.kt @@ -1,54 +1,70 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.preview.pdf -import androidx.lifecycle.Lifecycle -import androidx.test.espresso.intent.rule.IntentsTestRule +import androidx.annotation.UiThread +import androidx.test.core.app.launchActivity +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot import com.nextcloud.test.TestActivity import com.owncloud.android.AbstractIT import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.utils.ScreenshotTest -import org.junit.Rule +import org.junit.After +import org.junit.Before import org.junit.Test class PreviewPdfFragmentScreenshotIT : AbstractIT() { + private val testClassName = "com.owncloud.android.ui.preview.pdf.PreviewPdfFragmentScreenshotIT" companion object { private const val PDF_FILE_ASSET = "test.pdf" } - @get:Rule - val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false) + @Before + fun registerIdlingResource() { + IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource) + } + + @After + fun unregisterIdlingResource() { + IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource) + } @Test + @UiThread @ScreenshotTest fun showPdf() { - val activity = testActivityRule.launchActivity(null) + launchActivity().use { scenario -> + scenario.onActivity { activity -> + onIdleSync { + EspressoIdlingResource.increment() + val pdfFile = getFile(PDF_FILE_ASSET) + val ocFile = OCFile("/test.pdf").apply { + storagePath = pdfFile.absolutePath + } - val pdfFile = getFile(PDF_FILE_ASSET) - val ocFile = OCFile("/test.pdf").apply { - storagePath = pdfFile.absolutePath + val sut = PreviewPdfFragment.newInstance(ocFile) + activity.addFragment(sut) + sut.dismissSnack() + + EspressoIdlingResource.decrement() + + val screenShotName = createName(testClassName + "_" + "showPdf", "") + onView(isRoot()).check(matches(isDisplayed())) + screenshotViaName(activity, screenShotName) + } + } } - - val sut = PreviewPdfFragment.newInstance(ocFile) - activity.addFragment(sut) - - while (!sut.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { - shortSleep() - } - - activity.runOnUiThread { - sut.dismissSnack() - } - - shortSleep() - waitForIdleSync() - - screenshot(activity) } } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/trashbin/TrashbinActivityIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/trashbin/TrashbinActivityIT.kt index 8f3abf5..d18188a 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/trashbin/TrashbinActivityIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/trashbin/TrashbinActivityIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.trashbin @@ -20,6 +20,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isRoot import com.owncloud.android.utils.EspressoIdlingResource import com.owncloud.android.AbstractIT import com.owncloud.android.MainApp +import com.owncloud.android.extensions.launchAndCapture import com.owncloud.android.lib.common.accounts.AccountUtils import com.owncloud.android.utils.ScreenshotTest import org.junit.After @@ -30,7 +31,9 @@ class TrashbinActivityIT : AbstractIT() { private val testClassName = "com.owncloud.android.ui.trashbin.TrashbinActivityIT" enum class TestCase { - ERROR, EMPTY, FILES + ERROR, + EMPTY, + FILES } @Before @@ -69,84 +72,50 @@ class TrashbinActivityIT : AbstractIT() { @UiThread @ScreenshotTest fun files() { - launchActivity().use { scenario -> - scenario.onActivity { sut -> - val trashbinRepository = TrashbinLocalRepository(TestCase.FILES) - sut.trashbinPresenter = TrashbinPresenter(trashbinRepository, sut) - onIdleSync { - EspressoIdlingResource.increment() - sut.loadFolder( - onComplete = { EspressoIdlingResource.decrement() }, - onError = { EspressoIdlingResource.decrement() } - ) - onView(isRoot()).check(matches(isDisplayed())) - val screenShotName = createName(testClassName + "_" + "files", "") - screenshotViaName(sut, screenShotName) - } - } - } + launchAndCapture(testClassName, "files", before = { sut -> + val trashbinRepository = TrashbinLocalRepository(TestCase.FILES) + sut.trashbinPresenter = TrashbinPresenter(trashbinRepository, sut) + sut.loadFolder( + onComplete = { EspressoIdlingResource.decrement() }, + onError = { EspressoIdlingResource.decrement() } + ) + }) } @Test @UiThread @ScreenshotTest fun empty() { - launchActivity().use { scenario -> - scenario.onActivity { sut -> - val trashbinRepository = TrashbinLocalRepository(TestCase.EMPTY) - sut.trashbinPresenter = TrashbinPresenter(trashbinRepository, sut) - onIdleSync { - EspressoIdlingResource.increment() - sut.loadFolder( - onComplete = { EspressoIdlingResource.decrement() }, - onError = { EspressoIdlingResource.decrement() } - ) - onView(isRoot()).check(matches(isDisplayed())) - val screenShotName = createName(testClassName + "_" + "empty", "") - screenshotViaName(sut, screenShotName) - } - } - } + launchAndCapture(testClassName, "empty", before = { sut -> + val trashbinRepository = TrashbinLocalRepository(TestCase.EMPTY) + sut.trashbinPresenter = TrashbinPresenter(trashbinRepository, sut) + sut.loadFolder( + onComplete = { EspressoIdlingResource.decrement() }, + onError = { EspressoIdlingResource.decrement() } + ) + }) } @Test @UiThread @ScreenshotTest fun loading() { - launchActivity().use { scenario -> - scenario.onActivity { sut -> - val trashbinRepository = TrashbinLocalRepository(TestCase.EMPTY) - sut.trashbinPresenter = TrashbinPresenter(trashbinRepository, sut) - onIdleSync { - EspressoIdlingResource.increment() - sut.showInitialLoading() - EspressoIdlingResource.decrement() - val screenShotName = createName(testClassName + "_" + "loading", "") - onView(isRoot()).check(matches(isDisplayed())) - screenshotViaName(sut, screenShotName) - } - } - } + launchAndCapture(testClassName, "loading", before = { sut -> + val trashbinRepository = TrashbinLocalRepository(TestCase.EMPTY) + sut.trashbinPresenter = TrashbinPresenter(trashbinRepository, sut) + sut.showInitialLoading() + }) } @Test @UiThread @ScreenshotTest fun normalUser() { - launchActivity().use { scenario -> - scenario.onActivity { sut -> - val trashbinRepository = TrashbinLocalRepository(TestCase.EMPTY) - sut.trashbinPresenter = TrashbinPresenter(trashbinRepository, sut) - onIdleSync { - EspressoIdlingResource.increment() - sut.showUser() - EspressoIdlingResource.decrement() - val screenShotName = createName(testClassName + "_" + "normalUser", "") - onView(isRoot()).check(matches(isDisplayed())) - screenshotViaName(sut, screenShotName) - } - } - } + launchAndCapture(testClassName, "normalUser", before = { sut -> + val trashbinRepository = TrashbinLocalRepository(TestCase.EMPTY) + sut.trashbinPresenter = TrashbinPresenter(trashbinRepository, sut) + sut.showUser() + }) } @Test @@ -165,19 +134,10 @@ class TrashbinActivityIT : AbstractIT() { putExtra(Intent.EXTRA_USER, "differentUser@https://nextcloud.localhost") } - launchActivity(intent).use { scenario -> - scenario.onActivity { sut -> - val trashbinRepository = TrashbinLocalRepository(TestCase.EMPTY) - sut.trashbinPresenter = TrashbinPresenter(trashbinRepository, sut) - onIdleSync { - EspressoIdlingResource.increment() - sut.showUser() - EspressoIdlingResource.decrement() - val screenShotName = createName(testClassName + "_" + "differentUser", "") - onView(isRoot()).check(matches(isDisplayed())) - screenshotViaName(sut, screenShotName) - } - } - } + launchAndCapture(testClassName, "differentUser", intent = intent, before = { sut -> + val trashbinRepository = TrashbinLocalRepository(TestCase.EMPTY) + sut.trashbinPresenter = TrashbinPresenter(trashbinRepository, sut) + sut.showUser() + }) } } diff --git a/app/src/androidTest/java/com/owncloud/android/ui/trashbin/TrashbinLocalRepository.kt b/app/src/androidTest/java/com/owncloud/android/ui/trashbin/TrashbinLocalRepository.kt index 7042734..ebf5244 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/trashbin/TrashbinLocalRepository.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/trashbin/TrashbinLocalRepository.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.trashbin @@ -36,8 +36,10 @@ class TrashbinLocalRepository(private val testCase: TrashbinActivityIT.TestCase) "image/png", "/trashbin/test.png", "subFolder/test.png", - 1395847838, // random date - 1395847908 // random date + // random date + 1395847838, + // random date + 1395847908 ) ) files.add( @@ -46,8 +48,10 @@ class TrashbinLocalRepository(private val testCase: TrashbinActivityIT.TestCase) "image/jpeg", "/trashbin/image.jpg", "image.jpg", - 1395841858, // random date - 1395837858 // random date + // random date + 1395841858, + // random date + 1395837858 ) ) files.add( @@ -56,8 +60,10 @@ class TrashbinLocalRepository(private val testCase: TrashbinActivityIT.TestCase) "DIR", "/trashbin/folder/", "folder", - 1395347858, // random date - 1395849858 // random date + // random date + 1395347858, + // random date + 1395849858 ) ) diff --git a/app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java b/app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java index 8c81d28..15d6e3f 100644 --- a/app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java +++ b/app/src/androidTest/java/com/owncloud/android/util/EncryptionTestIT.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2017 Tobias Kaminsky * SPDX-FileCopyrightText: 2017 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.util; @@ -25,6 +25,7 @@ import com.owncloud.android.datamodel.e2e.v1.decrypted.Encrypted; import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFolderMetadataFileV1; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.e2ee.CsrHelper; +import com.owncloud.android.utils.crypto.CryptoHelper; import com.owncloud.android.utils.EncryptionUtils; import org.junit.Assert; @@ -44,7 +45,6 @@ import java.security.PrivateKey; import java.security.SecureRandom; import java.security.interfaces.RSAPrivateCrtKey; import java.security.interfaces.RSAPublicKey; -import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -57,7 +57,6 @@ import javax.crypto.Cipher; import static com.owncloud.android.utils.EncryptionUtils.decodeStringToBase64Bytes; import static com.owncloud.android.utils.EncryptionUtils.decryptFile; import static com.owncloud.android.utils.EncryptionUtils.decryptFolderMetaData; -import static com.owncloud.android.utils.EncryptionUtils.decryptPrivateKey; import static com.owncloud.android.utils.EncryptionUtils.decryptStringAsymmetric; import static com.owncloud.android.utils.EncryptionUtils.decryptStringSymmetric; import static com.owncloud.android.utils.EncryptionUtils.deserializeJSON; @@ -79,6 +78,7 @@ import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertArrayEquals; public class EncryptionTestIT extends AbstractIT { @Rule public RetryTestRule retryTestRule = new RetryTestRule(); @@ -149,7 +149,7 @@ public class EncryptionTestIT extends AbstractIT { byte[] key2 = decodeStringToBase64Bytes(decryptedString); - assertTrue(Arrays.equals(key1, key2)); + assertArrayEquals(key1, key2); } @Test @@ -164,7 +164,7 @@ public class EncryptionTestIT extends AbstractIT { byte[] key2 = decodeStringToBase64Bytes(decryptedString); - assertTrue(Arrays.equals(key1, key2)); + assertArrayEquals(key1, key2); } @Test(expected = BadPaddingException.class) @@ -261,13 +261,8 @@ public class EncryptionTestIT extends AbstractIT { byte[] privateKeyBytes = privateKey.getEncoded(); String privateKeyString = encodeBytesToBase64String(privateKeyBytes); - String encryptedString; - if (new Random().nextBoolean()) { - encryptedString = EncryptionUtils.encryptPrivateKey(privateKeyString, keyPhrase); - } else { - encryptedString = EncryptionUtils.encryptPrivateKeyOld(privateKeyString, keyPhrase); - } - String decryptedString = decryptPrivateKey(encryptedString, keyPhrase); + String encryptedString = CryptoHelper.INSTANCE.encryptPrivateKey(privateKeyString, keyPhrase); + String decryptedString = CryptoHelper.INSTANCE.decryptPrivateKey(encryptedString, keyPhrase); assertEquals(privateKeyString, decryptedString); } @@ -502,7 +497,7 @@ public class EncryptionTestIT extends AbstractIT { // de-serialize EncryptedFolderMetadataFileV1 encryptedFolderMetadata2 = deserializeJSON(encryptedJson, - new TypeToken() { + new TypeToken<>() { }); // decrypt diff --git a/app/src/androidTest/java/com/owncloud/android/util/ErrorMessageAdapterIT.java b/app/src/androidTest/java/com/owncloud/android/util/ErrorMessageAdapterIT.java index f072001..1e6b4ee 100644 --- a/app/src/androidTest/java/com/owncloud/android/util/ErrorMessageAdapterIT.java +++ b/app/src/androidTest/java/com/owncloud/android/util/ErrorMessageAdapterIT.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2021-2022 Chris Narkiewicz * SPDX-FileCopyrightText: 2019-2021 Tobias Kaminsky - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.util; diff --git a/app/src/androidTest/java/com/owncloud/android/utils/BitmapUtilsIT.kt b/app/src/androidTest/java/com/owncloud/android/utils/BitmapUtilsIT.kt index ac851a0..d9cbcf8 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/BitmapUtilsIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/BitmapUtilsIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.utils diff --git a/app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsIT.kt b/app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsIT.kt index de318ad..df69c14 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/DisplayUtilsIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.utils diff --git a/app/src/androidTest/java/com/owncloud/android/utils/DrawableUtilTests.kt b/app/src/androidTest/java/com/owncloud/android/utils/DrawableUtilTests.kt index f4542e7..d750163 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/DrawableUtilTests.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/DrawableUtilTests.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.utils @@ -12,18 +12,15 @@ import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable import androidx.test.platform.app.InstrumentationRegistry import org.junit.After -import org.junit.Assert.fail import org.junit.Before import org.junit.Test class DrawableUtilTests { - private var sut: DrawableUtil? = null private var context: Context? = null @Before fun setUp() { - sut = DrawableUtil() context = InstrumentationRegistry.getInstrumentation().context } @@ -32,18 +29,13 @@ class DrawableUtilTests { val bitmap: Bitmap = Bitmap.createBitmap(2, 2, Bitmap.Config.ARGB_8888) val drawable = BitmapDrawable(context?.resources, bitmap) - val layerDrawable = sut?.addDrawableAsOverlay(drawable, drawable) + val layerDrawable = DrawableUtil.addDrawableAsOverlay(drawable, drawable) - if (layerDrawable == null) { - fail("Layer drawable expected to be not null") - } - - assert(layerDrawable?.numberOfLayers == 2) + assert(layerDrawable.numberOfLayers == 2) } @After fun destroy() { - sut = null context = null } } diff --git a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionTestUtils.kt b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionTestUtils.kt index 0b0bf78..34d6621 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionTestUtils.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionTestUtils.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.utils @@ -118,7 +118,7 @@ nDO4ew== ) val users = mutableListOf( - DecryptedUser(userId, cert) + DecryptedUser(userId, cert, null) ) // val filedrop = mutableMapOf( diff --git a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsIT.kt b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsIT.kt index e5b2cf9..50c9815 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.utils diff --git a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt index 4d72bbd..3ab05fd 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/EncryptionUtilsV2IT.kt @@ -3,13 +3,14 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.utils import com.google.gson.reflect.TypeToken import com.nextcloud.client.account.MockUser import com.nextcloud.common.User +import com.nextcloud.utils.extensions.findMetadataKeyByUserId import com.owncloud.android.EncryptionIT import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.e2e.v1.decrypted.Data @@ -221,7 +222,7 @@ class EncryptionUtilsV2IT : EncryptionIT() { val metadataKeyBase64 = EncryptionUtils.generateKeyString() val metadataKey = EncryptionUtils.decodeStringToBase64Bytes(metadataKeyBase64) - val user = DecryptedUser("t1", encryptionTestUtils.t1PublicKey) + val user = DecryptedUser("t1", encryptionTestUtils.t1PublicKey, null) val encryptedUser = encryptionUtilsV2.encryptUser(user, metadataKey) assertNotEquals(encryptedUser.encryptedMetadataKey, metadataKeyBase64) @@ -274,6 +275,11 @@ class EncryptionUtilsV2IT : EncryptionIT() { arbitraryDataProvider ) + // V1 doesn't have decryptedMetadataKey so that we can ignore it for comparison + for (user in decrypted.users) { + user.decryptedMetadataKey = null + } + assertEquals(metadataFile, decrypted) } @@ -290,7 +296,8 @@ class EncryptionUtilsV2IT : EncryptionIT() { mimeType = MimeType.JPEG }, EncryptionUtils.generateIV(), - EncryptionUtils.generateUid(), // random string, not real tag + // random string, not real tag + EncryptionUtils.generateUid(), EncryptionUtils.generateKey(), metadataFile, storageManager @@ -404,8 +411,8 @@ class EncryptionUtilsV2IT : EncryptionIT() { assertTrue(true) // if we reach this, test is successful } - private fun generateDecryptedFileV1(): com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile { - return com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile().apply { + private fun generateDecryptedFileV1(): com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile = + com.owncloud.android.datamodel.e2e.v1.decrypted.DecryptedFile().apply { encrypted = Data().apply { key = EncryptionUtils.generateKeyString() filename = "Random filename.jpg" @@ -415,7 +422,6 @@ class EncryptionUtilsV2IT : EncryptionIT() { initializationVector = EncryptionUtils.generateKeyString() authenticationTag = EncryptionUtils.generateKeyString() } - } @Test fun testMigrateDecryptedV1ToV2() { @@ -488,7 +494,7 @@ class EncryptionUtilsV2IT : EncryptionIT() { var metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) - metadataFile = encryptionUtilsV2.addShareeToMetadata(metadataFile, enc2.accountName, enc2Cert) + metadataFile = encryptionUtilsV2.addShareeToMetadata(metadataFile, enc2.accountName, enc2Cert, null) val encryptedMetadataFile = encryptionUtilsV2.encryptFolderMetadataFile( metadataFile, @@ -540,7 +546,12 @@ class EncryptionUtilsV2IT : EncryptionIT() { val enc1 = MockUser("enc1", "Nextcloud") val enc2 = MockUser("enc2", "Nextcloud") var metadataFile = generateDecryptedFolderMetadataFile(enc1, enc1Cert) - metadataFile = encryptionUtilsV2.addShareeToMetadata(metadataFile, enc2.accountName, enc2Cert) + metadataFile = encryptionUtilsV2.addShareeToMetadata( + metadataFile, + enc2.accountName, + enc2Cert, + metadataFile.users.findMetadataKeyByUserId(enc2.accountName) + ) assertEquals(2, metadataFile.users.size) @@ -585,7 +596,7 @@ class EncryptionUtilsV2IT : EncryptionIT() { ) val users = mutableListOf( - DecryptedUser(user.accountName, cert) + DecryptedUser(user.accountName, cert, null) ) metadata.keyChecksums.add(encryptionUtilsV2.hashMetadataKey(metadata.metadataKey)) @@ -733,8 +744,6 @@ class EncryptionUtilsV2IT : EncryptionIT() { |Rei/RGBQ==","userId": "john"}],"version": "2"} """.trimMargin() - val base64Metadata = EncryptionUtils.encodeStringToBase64String(metadata) - val privateKey = EncryptionUtils.PEMtoPrivateKey(encryptionTestUtils.t1PrivateKey) val certificateT1 = EncryptionUtils.convertCertFromString(encryptionTestUtils.t1PublicKey) val certificateEnc2 = EncryptionUtils.convertCertFromString(enc2Cert) @@ -745,23 +754,18 @@ class EncryptionUtilsV2IT : EncryptionIT() { metadata ) - val base64Ans = encryptionUtilsV2.extractSignedString(signed) - - // verify val certs = listOf( certificateEnc2, certificateT1 ) - assertTrue(encryptionUtilsV2.verifySignedMessage(signed, certs)) - assertTrue(encryptionUtilsV2.verifySignedMessage(base64Ans, base64Metadata, certs)) + + assertTrue(encryptionUtilsV2.verifySignedData(signed, certs)) } @Throws(Throwable::class) @Test fun sign() { val sut = "randomstring123" - val json = "randomstring123" - val jsonBase64 = EncryptionUtils.encodeStringToBase64String(json) val privateKey = EncryptionUtils.PEMtoPrivateKey(encryptionTestUtils.t1PrivateKey) val certificate = EncryptionUtils.convertCertFromString(encryptionTestUtils.t1PublicKey) @@ -772,15 +776,12 @@ class EncryptionUtilsV2IT : EncryptionIT() { sut ) - val base64Ans = encryptionUtilsV2.extractSignedString(signed) - - // verify val certs = listOf( EncryptionUtils.convertCertFromString(enc2Cert), certificate ) - assertTrue(encryptionUtilsV2.verifySignedMessage(signed, certs)) - assertTrue(encryptionUtilsV2.verifySignedMessage(base64Ans, jsonBase64, certs)) + + assertTrue(encryptionUtilsV2.verifySignedData(signed, certs)) } @Test @@ -856,6 +857,11 @@ class EncryptionUtilsV2IT : EncryptionIT() { arbitraryDataProvider ) + // V1 doesn't have decryptedMetadataKey so that we can ignore it for comparison + for (user in decryptedFolderMetadata2.users) { + user.decryptedMetadataKey = null + } + // compare assertTrue( EncryptionTestIT.compareJsonStrings( diff --git a/app/src/androidTest/java/com/owncloud/android/utils/EspressoIdlingResource.kt b/app/src/androidTest/java/com/owncloud/android/utils/EspressoIdlingResource.kt index e01c833..0b05ee9 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/EspressoIdlingResource.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/EspressoIdlingResource.kt @@ -1,7 +1,7 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ diff --git a/app/src/androidTest/java/com/owncloud/android/utils/FileExportUtilsIT.kt b/app/src/androidTest/java/com/owncloud/android/utils/FileExportUtilsIT.kt index d7c81b9..2aa84ed 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/FileExportUtilsIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/FileExportUtilsIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.utils diff --git a/app/src/androidTest/java/com/owncloud/android/utils/FileStorageUtilsIT.kt b/app/src/androidTest/java/com/owncloud/android/utils/FileStorageUtilsIT.kt index 36453fe..04b5dba 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/FileStorageUtilsIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/FileStorageUtilsIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.utils @@ -141,7 +141,6 @@ class FileStorageUtilsIT : AbstractIT() { assertEquals("Internal storage/", pathToUserFriendlyDisplay("/storage/emulated/0/")) } - private fun pathToUserFriendlyDisplay(path: String): String { - return pathToUserFriendlyDisplay(path, targetContext, targetContext.resources) - } + private fun pathToUserFriendlyDisplay(path: String): String = + pathToUserFriendlyDisplay(path, targetContext, targetContext.resources) } diff --git a/app/src/androidTest/java/com/owncloud/android/utils/FileUtilTest.kt b/app/src/androidTest/java/com/owncloud/android/utils/FileUtilTest.kt index b44487f..6c27e85 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/FileUtilTest.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/FileUtilTest.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Andy Scherzinger - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.utils diff --git a/app/src/androidTest/java/com/owncloud/android/utils/SessionMixinTest.kt b/app/src/androidTest/java/com/owncloud/android/utils/SessionMixinTest.kt index a0e33a8..11914ed 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/SessionMixinTest.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/SessionMixinTest.kt @@ -1,7 +1,7 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ package com.owncloud.android.utils diff --git a/app/src/androidTest/java/com/owncloud/android/utils/SyncedFolderUtilsTest.kt b/app/src/androidTest/java/com/owncloud/android/utils/SyncedFolderUtilsTest.kt index 5e31285..310affd 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/SyncedFolderUtilsTest.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/SyncedFolderUtilsTest.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Andy Scherzinger - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.utils @@ -11,6 +11,7 @@ import com.owncloud.android.AbstractIT import com.owncloud.android.datamodel.MediaFolder import com.owncloud.android.datamodel.MediaFolderType import com.owncloud.android.datamodel.SyncedFolder +import com.owncloud.android.utils.SyncedFolderUtils.hasExcludePrefix import org.apache.commons.io.FileUtils import org.junit.AfterClass import org.junit.Assert @@ -205,6 +206,21 @@ class SyncedFolderUtilsTest : AbstractIT() { Assert.assertFalse(SyncedFolderUtils.isQualifyingMediaFolder(folder)) } + @Test + fun testInstantUploadPathIgnoreExcludedPrefixes() { + val testFiles = listOf( + "IMG_nnn.jpg", + "my_documents", + "Music", + ".trashed_IMG_nnn.jpg", + ".pending_IMG_nnn.jpg", + ".nomedia", + ".thumbdata_IMG_nnn", + ".thumbnail" + ).filter { !hasExcludePrefix(it) } + Assert.assertTrue(testFiles.size == 3) + } + companion object { private const val SELFIE = "selfie.png" private const val SCREENSHOT = "screenshot.JPG" diff --git a/app/src/androidTest/java/com/owncloud/android/utils/theme/CapabilityUtilsIT.kt b/app/src/androidTest/java/com/owncloud/android/utils/theme/CapabilityUtilsIT.kt index 98b410f..c27e104 100644 --- a/app/src/androidTest/java/com/owncloud/android/utils/theme/CapabilityUtilsIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/utils/theme/CapabilityUtilsIT.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.utils.theme @@ -17,9 +17,12 @@ import org.junit.Test class CapabilityUtilsIT : AbstractIT() { @Test fun checkOutdatedWarning() { - assertFalse(test(NextcloudVersion.nextcloud_28)) - assertFalse(test(NextcloudVersion.nextcloud_27)) + assertFalse(test(NextcloudVersion.nextcloud_31)) + assertFalse(test(NextcloudVersion.nextcloud_30)) + assertTrue(test(NextcloudVersion.nextcloud_29)) + assertTrue(test(NextcloudVersion.nextcloud_28)) + assertTrue(test(NextcloudVersion.nextcloud_27)) assertTrue(test(NextcloudVersion.nextcloud_26)) assertTrue(test(NextcloudVersion.nextcloud_25)) assertTrue(test(NextcloudVersion.nextcloud_24)) @@ -27,13 +30,16 @@ class CapabilityUtilsIT : AbstractIT() { assertTrue(test(NextcloudVersion.nextcloud_22)) assertTrue(test(NextcloudVersion.nextcloud_21)) assertTrue(test(OwnCloudVersion.nextcloud_20)) - assertTrue(test(OwnCloudVersion.nextcloud_19)) - assertTrue(test(OwnCloudVersion.nextcloud_18)) - assertTrue(test(OwnCloudVersion.nextcloud_17)) - assertTrue(test(OwnCloudVersion.nextcloud_16)) } - private fun test(version: OwnCloudVersion): Boolean { - return CapabilityUtils.checkOutdatedWarning(targetContext.resources, version, false) + @Test + fun checkOutdatedWarningWithSubscription() { + assertFalse(test(NextcloudVersion.nextcloud_31)) + assertFalse(test(NextcloudVersion.nextcloud_30)) + + assertFalse(test(OwnCloudVersion.nextcloud_20, true)) } + + private fun test(version: OwnCloudVersion, hasValidSubscription: Boolean = false): Boolean = + CapabilityUtils.checkOutdatedWarning(targetContext.resources, version, false, hasValidSubscription) } diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index 039d15e..4627fc2 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -3,7 +3,7 @@ ~ Nextcloud - Android Client ~ ~ SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors - ~ SPDX-License-Identifier: AGPL-3.0-or-later + ~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only --> diff --git a/app/src/debug/java/com/nextcloud/client/di/BuildTypeComponentsModule.kt b/app/src/debug/java/com/nextcloud/client/di/BuildTypeComponentsModule.kt index 2dcce1e..b3022c7 100644 --- a/app/src/debug/java/com/nextcloud/client/di/BuildTypeComponentsModule.kt +++ b/app/src/debug/java/com/nextcloud/client/di/BuildTypeComponentsModule.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.di diff --git a/app/src/debug/java/com/nextcloud/test/InjectionTestActivity.kt b/app/src/debug/java/com/nextcloud/test/InjectionTestActivity.kt index f9061f7..2e62f36 100644 --- a/app/src/debug/java/com/nextcloud/test/InjectionTestActivity.kt +++ b/app/src/debug/java/com/nextcloud/test/InjectionTestActivity.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Álvaro Brey * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.test @@ -17,7 +17,9 @@ import javax.inject.Inject /** * Sample activity to check test overriding injections */ -class InjectionTestActivity : AppCompatActivity(), Injectable { +class InjectionTestActivity : + AppCompatActivity(), + Injectable { @Inject lateinit var appPreferences: AppPreferences diff --git a/app/src/debug/java/com/nextcloud/test/TestActivity.kt b/app/src/debug/java/com/nextcloud/test/TestActivity.kt index 54a789f..e44aded 100644 --- a/app/src/debug/java/com/nextcloud/test/TestActivity.kt +++ b/app/src/debug/java/com/nextcloud/test/TestActivity.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.test @@ -42,17 +42,13 @@ class TestActivity : private lateinit var binding: TestLayoutBinding val connectivityServiceMock: ConnectivityService = object : ConnectivityService { - override fun isConnected(): Boolean { - return false - } + override fun isNetworkAndServerAvailable(callback: ConnectivityService.GenericCallback) = Unit - override fun isInternetWalled(): Boolean { - return false - } + override fun isConnected(): Boolean = false - override fun getConnectivity(): Connectivity { - return Connectivity.CONNECTED_WIFI - } + override fun isInternetWalled(): Boolean = false + + override fun getConnectivity(): Connectivity = Connectivity.CONNECTED_WIFI } override fun onCreate(savedInstanceState: Bundle?) { @@ -96,9 +92,7 @@ class TestActivity : TODO("Not yet implemented") } - override fun getOperationsServiceBinder(): OperationsService.OperationsServiceBinder? { - return null - } + override fun getOperationsServiceBinder(): OperationsService.OperationsServiceBinder? = null override fun showSortListGroup(show: Boolean) { // not needed @@ -112,13 +106,9 @@ class TestActivity : TODO("Not yet implemented") } - override fun getFileUploaderHelper(): FileUploadHelper { - return FileUploadHelper.instance() - } + override fun getFileUploaderHelper(): FileUploadHelper = FileUploadHelper.instance() - override fun getFileDownloadProgressListener(): FileDownloadWorker.FileDownloadProgressListener? { - return null - } + override fun getFileDownloadProgressListener(): FileDownloadWorker.FileDownloadProgressListener? = null override fun getStorageManager(): FileDataStorageManager { if (!this::storage.isInitialized) { diff --git a/app/src/debug/res/layout/activity_injection_test.xml b/app/src/debug/res/layout/activity_injection_test.xml index 522b666..2f83d13 100644 --- a/app/src/debug/res/layout/activity_injection_test.xml +++ b/app/src/debug/res/layout/activity_injection_test.xml @@ -4,7 +4,7 @@ ~ ~ SPDX-FileCopyrightText: 2023 Álvaro Brey ~ SPDX-FileCopyrightText: 2023 Nextcloud GmbH - ~ SPDX-License-Identifier: AGPL-3.0-or-later + ~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only --> ~ SPDX-FileCopyrightText: 2020 Nextcloud GmbH - ~ SPDX-License-Identifier: AGPL-3.0-or-later + ~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only --> * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.android.appReview @@ -11,8 +11,7 @@ import androidx.appcompat.app.AppCompatActivity import com.nextcloud.appReview.InAppReviewHelper import com.nextcloud.client.preferences.AppPreferences -class InAppReviewHelperImpl(appPreferences: AppPreferences) : - InAppReviewHelper { +class InAppReviewHelperImpl(appPreferences: AppPreferences) : InAppReviewHelper { override fun resetAndIncrementAppRestartCounter() { } diff --git a/app/src/generic/java/com/nextcloud/client/di/VariantComponentsModule.java b/app/src/generic/java/com/nextcloud/client/di/VariantComponentsModule.java index 1dd19f3..1b2a178 100644 --- a/app/src/generic/java/com/nextcloud/client/di/VariantComponentsModule.java +++ b/app/src/generic/java/com/nextcloud/client/di/VariantComponentsModule.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.di; diff --git a/app/src/generic/java/com/nextcloud/client/di/VariantModule.kt b/app/src/generic/java/com/nextcloud/client/di/VariantModule.kt index b9ded1e..d73f39e 100644 --- a/app/src/generic/java/com/nextcloud/client/di/VariantModule.kt +++ b/app/src/generic/java/com/nextcloud/client/di/VariantModule.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Álvaro Brey * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.di @@ -16,7 +16,5 @@ import dagger.Reusable internal class VariantModule { @Provides @Reusable - fun scanOptionalFeature(): AppScanOptionalFeature { - return AppScanOptionalFeature.Stub - } + fun scanOptionalFeature(): AppScanOptionalFeature = AppScanOptionalFeature.Stub } diff --git a/app/src/generic/java/com/owncloud/android/utils/PushUtils.java b/app/src/generic/java/com/owncloud/android/utils/PushUtils.java index e6faf60..139377f 100644 --- a/app/src/generic/java/com/owncloud/android/utils/PushUtils.java +++ b/app/src/generic/java/com/owncloud/android/utils/PushUtils.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz * SPDX-FileCopyrightText: 2017 Mario Danic - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.utils; diff --git a/app/src/generic/java/com/owncloud/android/utils/SecurityUtils.java b/app/src/generic/java/com/owncloud/android/utils/SecurityUtils.java index 721f026..97b19a2 100644 --- a/app/src/generic/java/com/owncloud/android/utils/SecurityUtils.java +++ b/app/src/generic/java/com/owncloud/android/utils/SecurityUtils.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2018 Mario Danic - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.utils; diff --git a/app/src/gplay/AndroidManifest.xml b/app/src/gplay/AndroidManifest.xml index 2de3aea..d566a24 100644 --- a/app/src/gplay/AndroidManifest.xml +++ b/app/src/gplay/AndroidManifest.xml @@ -4,7 +4,7 @@ ~ ~ SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors ~ SPDX-FileCopyrightText: 2017 Mario Danic - ~ SPDX-License-Identifier: AGPL-3.0-or-later + ~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only --> @@ -13,6 +13,8 @@ android:name="android.permission.REQUEST_INSTALL_PACKAGES" tools:node="remove"/> + + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.android.appReview @@ -85,11 +85,7 @@ class InAppReviewHelperImpl(val appPreferences: AppPreferences) : InAppReviewHel } } - private fun launchAppReviewFlow( - manager: ReviewManager, - activity: AppCompatActivity, - reviewInfo: ReviewInfo - ) { + private fun launchAppReviewFlow(manager: ReviewManager, activity: AppCompatActivity, reviewInfo: ReviewInfo) { val flow = manager.launchReviewFlow(activity, reviewInfo) flow.addOnCompleteListener { _ -> // The flow has finished. The API does not indicate whether the user diff --git a/app/src/gplay/java/com/nextcloud/client/di/VariantComponentsModule.java b/app/src/gplay/java/com/nextcloud/client/di/VariantComponentsModule.java index 849c3ca..6c807b6 100644 --- a/app/src/gplay/java/com/nextcloud/client/di/VariantComponentsModule.java +++ b/app/src/gplay/java/com/nextcloud/client/di/VariantComponentsModule.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.di; diff --git a/app/src/gplay/java/com/nextcloud/client/di/VariantModule.kt b/app/src/gplay/java/com/nextcloud/client/di/VariantModule.kt index 8d7728a..627cb92 100644 --- a/app/src/gplay/java/com/nextcloud/client/di/VariantModule.kt +++ b/app/src/gplay/java/com/nextcloud/client/di/VariantModule.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Álvaro Brey * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.di @@ -17,9 +17,7 @@ import dagger.Reusable internal class VariantModule { @Provides @Reusable - fun scanOptionalFeature(): AppScanOptionalFeature { - return object : AppScanOptionalFeature() { - override fun getScanContract() = ScanPageContract() - } + fun scanOptionalFeature(): AppScanOptionalFeature = object : AppScanOptionalFeature() { + override fun getScanContract() = ScanPageContract() } } diff --git a/app/src/gplay/java/com/owncloud/android/authentication/ModifiedAuthenticatorActivity.java b/app/src/gplay/java/com/owncloud/android/authentication/ModifiedAuthenticatorActivity.java index 8d5222d..81e03ec 100644 --- a/app/src/gplay/java/com/owncloud/android/authentication/ModifiedAuthenticatorActivity.java +++ b/app/src/gplay/java/com/owncloud/android/authentication/ModifiedAuthenticatorActivity.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2017 Mario Danic - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.authentication; diff --git a/app/src/gplay/java/com/owncloud/android/services/firebase/NCFirebaseMessagingService.java b/app/src/gplay/java/com/owncloud/android/services/firebase/NCFirebaseMessagingService.java index f2dddfe..ce6beea 100644 --- a/app/src/gplay/java/com/owncloud/android/services/firebase/NCFirebaseMessagingService.java +++ b/app/src/gplay/java/com/owncloud/android/services/firebase/NCFirebaseMessagingService.java @@ -3,12 +3,14 @@ * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2017 Mario Danic - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.services.firebase; +import android.content.Intent; import android.text.TextUtils; +import com.google.firebase.messaging.Constants.MessageNotificationKeys; import com.google.firebase.messaging.FirebaseMessagingService; import com.google.firebase.messaging.RemoteMessage; import com.nextcloud.client.account.UserAccountManager; @@ -16,6 +18,7 @@ import com.nextcloud.client.jobs.BackgroundJobManager; import com.nextcloud.client.jobs.NotificationWork; import com.nextcloud.client.preferences.AppPreferences; import com.owncloud.android.R; +import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.utils.PushUtils; import java.util.Map; @@ -30,14 +33,55 @@ public class NCFirebaseMessagingService extends FirebaseMessagingService { @Inject UserAccountManager accountManager; @Inject BackgroundJobManager backgroundJobManager; + static final String TAG = "NCFirebaseMessagingService"; + + // Firebase Messaging may apparently use two intent extras to specify a notification message. + // + // See the following fragments in https://github.com/firebase/firebase-android-sdk/blob/releases/m144_1.release/ + // firebase-messaging/src/main/java/com/google/firebase/messaging/FirebaseMessagingService.java#L223 + // firebase-messaging/src/main/java/com/google/firebase/messaging/NotificationParams.java#L419 + // firebase-messaging/src/main/java/com/google/firebase/messaging/Constants.java#L158 + // + // The "old" key is not exposed in com.google.firebase.messaging.Constants.MessageNotificationKeys, + // so we need to define it ourselves. + static final String ENABLE_NOTIFICATION_OLD = MessageNotificationKeys.NOTIFICATION_PREFIX_OLD + "e"; + static final String ENABLE_NOTIFICATION_NEW = MessageNotificationKeys.ENABLE_NOTIFICATION; + @Override public void onCreate() { super.onCreate(); AndroidInjection.inject(this); } + @Override + public void handleIntent(Intent intent) { + Log_OC.d(TAG, "handleIntent - extras: " + + ENABLE_NOTIFICATION_NEW + ": " + intent.getExtras().getString(ENABLE_NOTIFICATION_NEW) + ", " + + ENABLE_NOTIFICATION_OLD + ": " + intent.getExtras().getString(ENABLE_NOTIFICATION_OLD)); + + // When the app is in background and one of the ENABLE_NOTIFICATION or ENABLE_NOTIFICATION_OLD extras is set + // to "1" in the intent sent from the FCM system code to the FirebaseMessagingService in the application, + // the FCM library code that handles the intent DOES NOT invoke the onMessageReceived method. + // It just displays the notification by itself. + // + // In our case the original FCM message contains dummy values "NEW_NOTIFICATION" and we need to get the + // message in onMessageReceived to decrypt it. + // + // So we cheat here a little, by telling the FCM library that the notification flag is not set. + // + // Code below depends on implementation details of the firebase-messaging library (Firebase Android SDK). + // https://github.com/firebase/firebase-android-sdk/tree/master/firebase-messaging + + intent.removeExtra(ENABLE_NOTIFICATION_OLD); + intent.removeExtra(ENABLE_NOTIFICATION_NEW); + intent.putExtra(ENABLE_NOTIFICATION_NEW, "0"); + + super.handleIntent(intent); + } + @Override public void onMessageReceived(@NonNull RemoteMessage remoteMessage) { + Log_OC.d(TAG, "onMessageReceived"); final Map data = remoteMessage.getData(); final String subject = data.get(NotificationWork.KEY_NOTIFICATION_SUBJECT); final String signature = data.get(NotificationWork.KEY_NOTIFICATION_SIGNATURE); @@ -48,6 +92,7 @@ public class NCFirebaseMessagingService extends FirebaseMessagingService { @Override public void onNewToken(@NonNull String newToken) { + Log_OC.d(TAG, "onNewToken"); super.onNewToken(newToken); if (!TextUtils.isEmpty(getResources().getString(R.string.push_server_url))) { diff --git a/app/src/gplay/java/com/owncloud/android/utils/GooglePlayUtils.kt b/app/src/gplay/java/com/owncloud/android/utils/GooglePlayUtils.kt index 77406b8..9a307dd 100644 --- a/app/src/gplay/java/com/owncloud/android/utils/GooglePlayUtils.kt +++ b/app/src/gplay/java/com/owncloud/android/utils/GooglePlayUtils.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2017 Mario Danic - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.utils diff --git a/app/src/gplay/java/com/owncloud/android/utils/PushUtils.java b/app/src/gplay/java/com/owncloud/android/utils/PushUtils.java index 22df021..4e77b40 100644 --- a/app/src/gplay/java/com/owncloud/android/utils/PushUtils.java +++ b/app/src/gplay/java/com/owncloud/android/utils/PushUtils.java @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2019 Tobias Kaminsky * SPDX-FileCopyrightText: 2019 Chris Narkiewicz * SPDX-FileCopyrightText: 2017-2018 Mario Danic - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.utils; @@ -19,6 +19,7 @@ import com.google.gson.Gson; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.client.preferences.AppPreferencesImpl; +import com.nextcloud.common.NextcloudClient; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.datamodel.ArbitraryDataProvider; @@ -26,9 +27,7 @@ import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; import com.owncloud.android.datamodel.PushConfigurationState; import com.owncloud.android.datamodel.SignatureVerification; import com.owncloud.android.lib.common.OwnCloudAccount; -import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.OwnCloudClientManagerFactory; -import com.owncloud.android.lib.common.operations.RemoteOperation; import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.notifications.RegisterAccountDeviceForNotificationsOperation; @@ -45,6 +44,7 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.nio.file.Files; import java.security.InvalidKeyException; import java.security.Key; import java.security.KeyFactory; @@ -97,7 +97,11 @@ public final class PushUtils { if (!new File(privateKeyPath).exists() && !new File(publicKeyPath).exists()) { try { if (!keyPathFile.exists()) { - keyPathFile.mkdir(); + try { + Files.createDirectory(keyPathFile.toPath()); + } catch (IOException e) { + Log_OC.e(TAG, "Could not create directory: " + keyPathFile.getAbsolutePath(), e); + } } KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(2048); @@ -131,14 +135,11 @@ public final class PushUtils { try { ocAccount = new OwnCloudAccount(account, context); - OwnCloudClient mClient = OwnCloudClientManagerFactory.getDefaultSingleton(). - getClientFor(ocAccount, context); + NextcloudClient mClient = OwnCloudClientManagerFactory.getDefaultSingleton(). + getNextcloudClientFor(ocAccount, context); - RemoteOperation unregisterAccountDeviceForNotificationsOperation = new - UnregisterAccountDeviceForNotificationsOperation(); - - RemoteOperationResult remoteOperationResult = unregisterAccountDeviceForNotificationsOperation. - execute(mClient); + RemoteOperationResult remoteOperationResult = + new UnregisterAccountDeviceForNotificationsOperation().execute(mClient); if (remoteOperationResult.getHttpCode() == HttpStatus.SC_ACCEPTED) { String arbitraryValue; @@ -201,8 +202,8 @@ public final class PushUtils { TextUtils.isEmpty(providerValue)) { try { OwnCloudAccount ocAccount = new OwnCloudAccount(account, context); - OwnCloudClient client = OwnCloudClientManagerFactory.getDefaultSingleton(). - getClientFor(ocAccount, context); + NextcloudClient client = OwnCloudClientManagerFactory.getDefaultSingleton(). + getNextcloudClientFor(ocAccount, context); RemoteOperationResult remoteOperationResult = new RegisterAccountDeviceForNotificationsOperation(pushTokenHash, @@ -308,8 +309,12 @@ public final class PushUtils { try { if (!new File(path).exists()) { File newFile = new File(path); - newFile.getParentFile().mkdirs(); - newFile.createNewFile(); + try { + Files.createDirectories(newFile.getParentFile().toPath()); + } catch (IOException e) { + Log_OC.e(TAG, "Could not create directory: " + newFile.getParentFile(), e); + } + Files.createFile(newFile.toPath()); } keyFileOutputStream = new FileOutputStream(path); keyFileOutputStream.write(encoded); diff --git a/app/src/gplay/java/com/owncloud/android/utils/SecurityUtils.java b/app/src/gplay/java/com/owncloud/android/utils/SecurityUtils.java index 954bba4..0ee14db 100644 --- a/app/src/gplay/java/com/owncloud/android/utils/SecurityUtils.java +++ b/app/src/gplay/java/com/owncloud/android/utils/SecurityUtils.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2018 Mario Danic - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.utils; diff --git a/app/src/gplay/res/values/setup.xml b/app/src/gplay/res/values/setup.xml index 1a44233..83fadc4 100644 --- a/app/src/gplay/res/values/setup.xml +++ b/app/src/gplay/res/values/setup.xml @@ -3,7 +3,7 @@ ~ Nextcloud - Android Client ~ ~ SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors - ~ SPDX-License-Identifier: AGPL-3.0-or-later + ~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only --> diff --git a/app/src/huawei/AndroidManifest.xml b/app/src/huawei/AndroidManifest.xml index 27021d0..0178cc6 100644 --- a/app/src/huawei/AndroidManifest.xml +++ b/app/src/huawei/AndroidManifest.xml @@ -4,7 +4,7 @@ ~ ~ SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors ~ SPDX-FileCopyrightText: 2021 Tobias Kaminsky - ~ SPDX-License-Identifier: AGPL-3.0-or-later + ~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only --> diff --git a/app/src/huawei/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt b/app/src/huawei/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt index 067e774..3422424 100644 --- a/app/src/huawei/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt +++ b/app/src/huawei/java/com/nextcloud/android/appReview/InAppReviewHelperImpl.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.android.appReview @@ -11,8 +11,7 @@ import androidx.appcompat.app.AppCompatActivity import com.nextcloud.appReview.InAppReviewHelper import com.nextcloud.client.preferences.AppPreferences -class InAppReviewHelperImpl(appPreferences: AppPreferences) : - InAppReviewHelper { +class InAppReviewHelperImpl(appPreferences: AppPreferences) : InAppReviewHelper { override fun resetAndIncrementAppRestartCounter() { } diff --git a/app/src/huawei/java/com/nextcloud/client/di/VariantComponentsModule.java b/app/src/huawei/java/com/nextcloud/client/di/VariantComponentsModule.java index 74822d8..ca426ec 100644 --- a/app/src/huawei/java/com/nextcloud/client/di/VariantComponentsModule.java +++ b/app/src/huawei/java/com/nextcloud/client/di/VariantComponentsModule.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.di; diff --git a/app/src/huawei/java/com/nextcloud/client/di/VariantModule.kt b/app/src/huawei/java/com/nextcloud/client/di/VariantModule.kt index 8d7728a..627cb92 100644 --- a/app/src/huawei/java/com/nextcloud/client/di/VariantModule.kt +++ b/app/src/huawei/java/com/nextcloud/client/di/VariantModule.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Álvaro Brey * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.di @@ -17,9 +17,7 @@ import dagger.Reusable internal class VariantModule { @Provides @Reusable - fun scanOptionalFeature(): AppScanOptionalFeature { - return object : AppScanOptionalFeature() { - override fun getScanContract() = ScanPageContract() - } + fun scanOptionalFeature(): AppScanOptionalFeature = object : AppScanOptionalFeature() { + override fun getScanContract() = ScanPageContract() } } diff --git a/app/src/huawei/java/com/owncloud/android/ui/activity/HuaweiCommunityActivity.kt b/app/src/huawei/java/com/owncloud/android/ui/activity/HuaweiCommunityActivity.kt index 6fb04ae..a5272c0 100644 --- a/app/src/huawei/java/com/owncloud/android/ui/activity/HuaweiCommunityActivity.kt +++ b/app/src/huawei/java/com/owncloud/android/ui/activity/HuaweiCommunityActivity.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activity diff --git a/app/src/huawei/java/com/owncloud/android/utils/PushUtils.java b/app/src/huawei/java/com/owncloud/android/utils/PushUtils.java index 3899810..bf1949a 100644 --- a/app/src/huawei/java/com/owncloud/android/utils/PushUtils.java +++ b/app/src/huawei/java/com/owncloud/android/utils/PushUtils.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz * SPDX-FileCopyrightText: 2017 Mario Danic - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.utils; diff --git a/app/src/huawei/java/com/owncloud/android/utils/SecurityUtils.java b/app/src/huawei/java/com/owncloud/android/utils/SecurityUtils.java index 721f026..97b19a2 100644 --- a/app/src/huawei/java/com/owncloud/android/utils/SecurityUtils.java +++ b/app/src/huawei/java/com/owncloud/android/utils/SecurityUtils.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2018 Mario Danic - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.utils; diff --git a/app/src/huawei/res/values/bools.xml b/app/src/huawei/res/values/bools.xml index 107d34c..7e8bb54 100644 --- a/app/src/huawei/res/values/bools.xml +++ b/app/src/huawei/res/values/bools.xml @@ -4,7 +4,7 @@ ~ ~ SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors ~ SPDX-FileCopyrightText: 2021 Tobias Kaminsky - ~ SPDX-License-Identifier: AGPL-3.0-or-later + ~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only --> false diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8b1b9cc..4154b1c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,14 +1,13 @@ - - - @@ -47,14 +46,16 @@ + - - - + + + + + @@ -188,10 +282,23 @@ + + + + @@ -235,6 +342,11 @@ android:name=".ui.activity.ContactsPreferenceActivity" android:exported="false" android:launchMode="singleInstance" /> + + + + + + + @@ -480,6 +603,10 @@ + + + android:label="@string/manage_space_title" /> - \ No newline at end of file + diff --git a/app/src/main/java/com/nextcloud/android/files/FileLockingHelper.kt b/app/src/main/java/com/nextcloud/android/files/FileLockingHelper.kt index 12b6cf2..5e3c7a6 100644 --- a/app/src/main/java/com/nextcloud/android/files/FileLockingHelper.kt +++ b/app/src/main/java/com/nextcloud/android/files/FileLockingHelper.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.android.files diff --git a/app/src/main/java/com/nextcloud/android/sso/Constants.java b/app/src/main/java/com/nextcloud/android/sso/Constants.java index a752b39..86dd2f8 100644 --- a/app/src/main/java/com/nextcloud/android/sso/Constants.java +++ b/app/src/main/java/com/nextcloud/android/sso/Constants.java @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2019 David Luhmer * SPDX-FileCopyrightText: 2019 Tobias Kaminsky * SPDX-FileCopyrightText: 2019 Edvard Holst - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.android.sso; diff --git a/app/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java b/app/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java index 6ef2e6e..31e061b 100644 --- a/app/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java +++ b/app/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 David Luhmer - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only * * More information here: https://github.com/abeluck/android-streams-ipc */ @@ -237,7 +237,7 @@ public class InputStreamBinder extends IInputStreamService.Stub { case "POST": method = new PostMethod(requestUrl); if (requestBodyInputStream != null) { - RequestEntity requestEntity = new InputStreamRequestEntity(requestBodyInputStream, -1); + RequestEntity requestEntity = new InputStreamRequestEntity(requestBodyInputStream); ((PostMethod) method).setRequestEntity(requestEntity); } else if (request.getRequestBody() != null) { StringRequestEntity requestEntity = new StringRequestEntity( @@ -251,7 +251,7 @@ public class InputStreamBinder extends IInputStreamService.Stub { case "PATCH": method = new PatchMethod(requestUrl); if (requestBodyInputStream != null) { - RequestEntity requestEntity = new InputStreamRequestEntity(requestBodyInputStream, -1); + RequestEntity requestEntity = new InputStreamRequestEntity(requestBodyInputStream); ((PatchMethod) method).setRequestEntity(requestEntity); } else if (request.getRequestBody() != null) { StringRequestEntity requestEntity = new StringRequestEntity( @@ -265,7 +265,7 @@ public class InputStreamBinder extends IInputStreamService.Stub { case "PUT": method = new PutMethod(requestUrl); if (requestBodyInputStream != null) { - RequestEntity requestEntity = new InputStreamRequestEntity(requestBodyInputStream, -1); + RequestEntity requestEntity = new InputStreamRequestEntity(requestBodyInputStream); ((PutMethod) method).setRequestEntity(requestEntity); } else if (request.getRequestBody() != null) { StringRequestEntity requestEntity = new StringRequestEntity( @@ -502,12 +502,15 @@ public class InputStreamBinder extends IInputStreamService.Stub { @VisibleForTesting public static NameValuePair[] convertMapToNVP(Map map) { - NameValuePair[] nvp = new NameValuePair[map.size()]; + final var nvp = new NameValuePair[map.size()]; int i = 0; - for (String key : map.keySet()) { - nvp[i] = new NameValuePair(key, map.get(key)); + + for (Map.Entry entry : map.entrySet()) { + final var nameValuePair = new NameValuePair(entry.getKey(), entry.getValue()); + nvp[i] = nameValuePair; i++; } + return nvp; } diff --git a/app/src/main/java/com/nextcloud/android/sso/PatchMethod.java b/app/src/main/java/com/nextcloud/android/sso/PatchMethod.java index 7637471..34c5e8b 100644 --- a/app/src/main/java/com/nextcloud/android/sso/PatchMethod.java +++ b/app/src/main/java/com/nextcloud/android/sso/PatchMethod.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2021 Timo Triebensky * SPDX-FileCopyrightText: 2021 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only * * More information here: https://github.com/abeluck/android-streams-ipc */ diff --git a/app/src/main/java/com/nextcloud/android/sso/PlainHeader.java b/app/src/main/java/com/nextcloud/android/sso/PlainHeader.java index b590706..07c23c6 100644 --- a/app/src/main/java/com/nextcloud/android/sso/PlainHeader.java +++ b/app/src/main/java/com/nextcloud/android/sso/PlainHeader.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2019 Tobias Kaminsky * SPDX-FileCopyrightText: 2019 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.android.sso; diff --git a/app/src/main/java/com/nextcloud/android/sso/QueryParam.java b/app/src/main/java/com/nextcloud/android/sso/QueryParam.java index 25e42dd..21f7644 100644 --- a/app/src/main/java/com/nextcloud/android/sso/QueryParam.java +++ b/app/src/main/java/com/nextcloud/android/sso/QueryParam.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2021 Tobias Kaminsky * SPDX-FileCopyrightText: 2021 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.android.sso; diff --git a/app/src/main/java/com/nextcloud/android/sso/Response.java b/app/src/main/java/com/nextcloud/android/sso/Response.java index 0bd992c..2402b8b 100644 --- a/app/src/main/java/com/nextcloud/android/sso/Response.java +++ b/app/src/main/java/com/nextcloud/android/sso/Response.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2019 Tobias Kaminsky * SPDX-FileCopyrightText: 2019 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.android.sso; diff --git a/app/src/main/java/com/nextcloud/android/sso/aidl/IThreadListener.java b/app/src/main/java/com/nextcloud/android/sso/aidl/IThreadListener.java index 18e5d84..cf4ab01 100644 --- a/app/src/main/java/com/nextcloud/android/sso/aidl/IThreadListener.java +++ b/app/src/main/java/com/nextcloud/android/sso/aidl/IThreadListener.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2017 David Luhmer - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.android.sso.aidl; diff --git a/app/src/main/java/com/nextcloud/android/sso/aidl/NextcloudRequest.java b/app/src/main/java/com/nextcloud/android/sso/aidl/NextcloudRequest.java index 4a3ebc7..2308e01 100644 --- a/app/src/main/java/com/nextcloud/android/sso/aidl/NextcloudRequest.java +++ b/app/src/main/java/com/nextcloud/android/sso/aidl/NextcloudRequest.java @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2021 Tobias Kaminsky * SPDX-FileCopyrightText: 2021 Nextcloud GmbH * SPDX-FileCopyrightText: 2017 David Luhmer - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.android.sso.aidl; diff --git a/app/src/main/java/com/nextcloud/android/sso/aidl/ParcelFileDescriptorUtil.java b/app/src/main/java/com/nextcloud/android/sso/aidl/ParcelFileDescriptorUtil.java index ac51d60..fbbebdb 100644 --- a/app/src/main/java/com/nextcloud/android/sso/aidl/ParcelFileDescriptorUtil.java +++ b/app/src/main/java/com/nextcloud/android/sso/aidl/ParcelFileDescriptorUtil.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2017 David Luhmer - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.android.sso.aidl; diff --git a/app/src/main/java/com/nextcloud/appReview/AppReviewShownModel.kt b/app/src/main/java/com/nextcloud/appReview/AppReviewShownModel.kt index e906835..bd0acc0 100644 --- a/app/src/main/java/com/nextcloud/appReview/AppReviewShownModel.kt +++ b/app/src/main/java/com/nextcloud/appReview/AppReviewShownModel.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.appReview diff --git a/app/src/main/java/com/nextcloud/appReview/InAppReviewHelper.kt b/app/src/main/java/com/nextcloud/appReview/InAppReviewHelper.kt index 200efc6..34a054b 100644 --- a/app/src/main/java/com/nextcloud/appReview/InAppReviewHelper.kt +++ b/app/src/main/java/com/nextcloud/appReview/InAppReviewHelper.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.appReview diff --git a/app/src/main/java/com/nextcloud/appReview/InAppReviewModule.kt b/app/src/main/java/com/nextcloud/appReview/InAppReviewModule.kt index 331ea93..c2f4e92 100644 --- a/app/src/main/java/com/nextcloud/appReview/InAppReviewModule.kt +++ b/app/src/main/java/com/nextcloud/appReview/InAppReviewModule.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.appReview @@ -18,7 +18,6 @@ class InAppReviewModule { @Provides @Singleton - internal fun providesInAppReviewHelper(appPreferences: AppPreferences): InAppReviewHelper { - return InAppReviewHelperImpl(appPreferences) - } + internal fun providesInAppReviewHelper(appPreferences: AppPreferences): InAppReviewHelper = + InAppReviewHelperImpl(appPreferences) } diff --git a/app/src/main/java/com/nextcloud/client/NominatimClient.kt b/app/src/main/java/com/nextcloud/client/NominatimClient.kt index d5b07fd..7d8f35b 100644 --- a/app/src/main/java/com/nextcloud/client/NominatimClient.kt +++ b/app/src/main/java/com/nextcloud/client/NominatimClient.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 ZetaTom * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client @@ -12,7 +12,7 @@ import com.google.gson.annotations.SerializedName import com.owncloud.android.MainApp import okhttp3.OkHttpClient import okhttp3.Request -import okhttp3.internal.http.HTTP_OK +import java.net.HttpURLConnection.HTTP_OK import java.net.URLEncoder class NominatimClient constructor(geocoderBaseUrl: String, email: String) { diff --git a/app/src/main/java/com/nextcloud/client/account/AnonymousUser.kt b/app/src/main/java/com/nextcloud/client/account/AnonymousUser.kt index 4df6a88..746e999 100644 --- a/app/src/main/java/com/nextcloud/client/account/AnonymousUser.kt +++ b/app/src/main/java/com/nextcloud/client/account/AnonymousUser.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.account @@ -23,7 +23,9 @@ import java.net.URI * It serves as a semantically correct "empty value", allowing simplification of logic * in various components requiring user data, such as DB queries. */ -internal data class AnonymousUser(private val accountType: String) : User, Parcelable { +internal data class AnonymousUser(private val accountType: String) : + User, + Parcelable { companion object { @JvmStatic @@ -47,21 +49,14 @@ internal data class AnonymousUser(private val accountType: String) : User, Parce override val server = Server(URI.create(""), MainApp.MINIMUM_SUPPORTED_SERVER_VERSION) override val isAnonymous = true - override fun toPlatformAccount(): Account { - return Account(accountName, accountType) - } + override fun toPlatformAccount(): Account = Account(accountName, accountType) - override fun toOwnCloudAccount(): OwnCloudAccount { - return OwnCloudAccount(Uri.EMPTY, OwnCloudBasicCredentials("", "")) - } + override fun toOwnCloudAccount(): OwnCloudAccount = OwnCloudAccount(Uri.EMPTY, OwnCloudBasicCredentials("", "")) - override fun nameEquals(user: User?): Boolean { - return user?.accountName.equals(accountName, true) - } + override fun nameEquals(user: User?): Boolean = user?.accountName.equals(accountName, true) - override fun nameEquals(accountName: CharSequence?): Boolean { - return accountName?.toString().equals(this.accountType, true) - } + override fun nameEquals(accountName: CharSequence?): Boolean = + accountName?.toString().equals(this.accountType, true) override fun describeContents() = 0 diff --git a/app/src/main/java/com/nextcloud/client/account/CurrentAccountProvider.java b/app/src/main/java/com/nextcloud/client/account/CurrentAccountProvider.java index 0ff0744..b74bb3f 100644 --- a/app/src/main/java/com/nextcloud/client/account/CurrentAccountProvider.java +++ b/app/src/main/java/com/nextcloud/client/account/CurrentAccountProvider.java @@ -2,14 +2,13 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.account; import android.accounts.Account; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; /** * This interface provides access to currently selected user. diff --git a/app/src/main/java/com/nextcloud/client/account/MockUser.kt b/app/src/main/java/com/nextcloud/client/account/MockUser.kt index 3b86547..7cbf717 100644 --- a/app/src/main/java/com/nextcloud/client/account/MockUser.kt +++ b/app/src/main/java/com/nextcloud/client/account/MockUser.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.account @@ -20,7 +20,9 @@ import java.net.URI * This is a mock user object suitable for integration tests. Mocks obtained from code generators * such as Mockito or MockK cannot be transported in Intent extras. */ -data class MockUser(override val accountName: String, val accountType: String) : User, Parcelable { +data class MockUser(override val accountName: String, val accountType: String) : + User, + Parcelable { constructor() : this(DEFAULT_MOCK_ACCOUNT_NAME, DEFAULT_MOCK_ACCOUNT_TYPE) @@ -42,21 +44,14 @@ data class MockUser(override val accountName: String, val accountType: String) : override val server = Server(URI.create(""), MainApp.MINIMUM_SUPPORTED_SERVER_VERSION) override val isAnonymous = false - override fun toPlatformAccount(): Account { - return Account(accountName, accountType) - } + override fun toPlatformAccount(): Account = Account(accountName, accountType) - override fun toOwnCloudAccount(): OwnCloudAccount { - return OwnCloudAccount(Uri.EMPTY, OwnCloudBasicCredentials("", "")) - } + override fun toOwnCloudAccount(): OwnCloudAccount = OwnCloudAccount(Uri.EMPTY, OwnCloudBasicCredentials("", "")) - override fun nameEquals(user: User?): Boolean { - return user?.accountName.equals(accountName, true) - } + override fun nameEquals(user: User?): Boolean = user?.accountName.equals(accountName, true) - override fun nameEquals(accountName: CharSequence?): Boolean { - return accountName?.toString().equals(this.accountType, true) - } + override fun nameEquals(accountName: CharSequence?): Boolean = + accountName?.toString().equals(this.accountType, true) override fun describeContents() = 0 diff --git a/app/src/main/java/com/nextcloud/client/account/RegisteredUser.kt b/app/src/main/java/com/nextcloud/client/account/RegisteredUser.kt index 8d81d16..d73a99a 100644 --- a/app/src/main/java/com/nextcloud/client/account/RegisteredUser.kt +++ b/app/src/main/java/com/nextcloud/client/account/RegisteredUser.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz * SPDX-FileCopyrightText: 2019 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.account @@ -41,21 +41,14 @@ internal data class RegisteredUser( return account.name } - override fun toPlatformAccount(): Account { - return account - } + override fun toPlatformAccount(): Account = account - override fun toOwnCloudAccount(): OwnCloudAccount { - return ownCloudAccount - } + override fun toOwnCloudAccount(): OwnCloudAccount = ownCloudAccount - override fun nameEquals(user: User?): Boolean { - return nameEquals(user?.accountName) - } + override fun nameEquals(user: User?): Boolean = nameEquals(user?.accountName) - override fun nameEquals(accountName: CharSequence?): Boolean { - return accountName?.toString().equals(this.accountName, true) - } + override fun nameEquals(accountName: CharSequence?): Boolean = + accountName?.toString().equals(this.accountName, true) override fun describeContents() = 0 diff --git a/app/src/main/java/com/nextcloud/client/account/Server.kt b/app/src/main/java/com/nextcloud/client/account/Server.kt index 87367b8..63f4dfb 100644 --- a/app/src/main/java/com/nextcloud/client/account/Server.kt +++ b/app/src/main/java/com/nextcloud/client/account/Server.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz * SPDX-FileCopyrightText: 2019 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.account diff --git a/app/src/main/java/com/nextcloud/client/account/User.kt b/app/src/main/java/com/nextcloud/client/account/User.kt index 9651b72..856bb0b 100644 --- a/app/src/main/java/com/nextcloud/client/account/User.kt +++ b/app/src/main/java/com/nextcloud/client/account/User.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.account @@ -11,7 +11,9 @@ import android.accounts.Account import android.os.Parcelable import com.owncloud.android.lib.common.OwnCloudAccount -interface User : Parcelable, com.nextcloud.common.User { +interface User : + Parcelable, + com.nextcloud.common.User { override val accountName: String val server: Server val isAnonymous: Boolean diff --git a/app/src/main/java/com/nextcloud/client/account/UserAccountManager.java b/app/src/main/java/com/nextcloud/client/account/UserAccountManager.java index 44156f6..a554987 100644 --- a/app/src/main/java/com/nextcloud/client/account/UserAccountManager.java +++ b/app/src/main/java/com/nextcloud/client/account/UserAccountManager.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.account; diff --git a/app/src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java b/app/src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java index 7557c39..2bcc027 100644 --- a/app/src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java +++ b/app/src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 TSI-mc + * SPDX-FileCopyrightText: 2023-2024 TSI-mc * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.account; @@ -18,10 +18,11 @@ import android.content.Intent; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.text.TextUtils; -import android.util.Log; +import com.nextcloud.client.onboarding.FirstRunActivity; import com.nextcloud.common.NextcloudClient; import com.nextcloud.utils.extensions.AccountExtensionsKt; +import com.nmc.android.ui.LauncherActivity; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.authentication.AuthenticatorActivity; @@ -112,25 +113,78 @@ public class UserAccountManagerImpl implements UserAccountManager { @Override public boolean exists(Account account) { - Account[] nextcloudAccounts = getAccounts(); + try { + if (account == null) { + Log_OC.d(TAG, "account is null"); + return false; + } + + Account[] nextcloudAccounts = getAccounts(); + if (nextcloudAccounts.length == 0) { + Log_OC.d(TAG, "nextcloudAccounts are empty"); + return false; + } + + if (account.name.isEmpty()) { + Log_OC.d(TAG, "account name is empty"); + return false; + } - if (account != null && account.name != null) { int lastAtPos = account.name.lastIndexOf('@'); + if (lastAtPos == -1) { + Log_OC.d(TAG, "lastAtPos cannot be found"); + return false; + } + + boolean isLastAtPosInBoundsForHostAndPort = lastAtPos + 1 < account.name.length(); + if (!isLastAtPosInBoundsForHostAndPort) { + Log_OC.d(TAG, "lastAtPos not in bounds"); + return false; + } + String hostAndPort = account.name.substring(lastAtPos + 1); + String username = account.name.substring(0, lastAtPos); + if (hostAndPort.isEmpty() || username.isEmpty()) { + Log_OC.d(TAG, "hostAndPort or username is empty"); + return false; + } + String otherHostAndPort; String otherUsername; + for (Account otherAccount : nextcloudAccounts) { + // Skip null accounts or accounts with null names + if (otherAccount == null || otherAccount.name.isEmpty()) { + continue; + } + lastAtPos = otherAccount.name.lastIndexOf('@'); + + // Skip invalid account names + if (lastAtPos == -1) { + continue; + } + + boolean isLastAtPosInBoundsForOtherHostAndPort = lastAtPos + 1 < otherAccount.name.length(); + if (!isLastAtPosInBoundsForOtherHostAndPort) { + continue; + } otherHostAndPort = otherAccount.name.substring(lastAtPos + 1); + otherUsername = otherAccount.name.substring(0, lastAtPos); + if (otherHostAndPort.equals(hostAndPort) && otherUsername.equalsIgnoreCase(username)) { return true; } } + + return false; + } catch (Exception e) { + Log_OC.d(TAG, "Exception caught at UserAccountManagerImpl.exists(): " + e); + return false; } - return false; } @Override @@ -180,19 +234,20 @@ public class UserAccountManagerImpl implements UserAccountManager { */ @Nullable private User createUserFromAccount(@NonNull Account account) { - if (AccountExtensionsKt.isAnonymous(account, context)) { + Context safeContext = context != null ? context : MainApp.getAppContext(); + if (safeContext == null) { + Log_OC.e(TAG, "Unable to obtain a valid context"); return null; } - if (context == null) { - Log_OC.d(TAG, "Context is null MainApp.getAppContext() used"); - context = MainApp.getAppContext(); + if (AccountExtensionsKt.isAnonymous(account, safeContext)) { + return null; } OwnCloudAccount ownCloudAccount; try { - ownCloudAccount = new OwnCloudAccount(account, context); - } catch (AccountUtils.AccountNotFoundException ex) { + ownCloudAccount = new OwnCloudAccount(account, safeContext); + } catch (Exception ex) { return null; } @@ -212,7 +267,7 @@ public class UserAccountManagerImpl implements UserAccountManager { */ String serverAddressStr = accountManager.getUserData(account, AccountUtils.Constants.KEY_OC_BASE_URL); if (serverAddressStr == null || serverAddressStr.isEmpty()) { - return AnonymousUser.fromContext(context); + return AnonymousUser.fromContext(safeContext); } URI serverUri = URI.create(serverAddressStr); // TODO: validate @@ -398,6 +453,10 @@ public class UserAccountManagerImpl implements UserAccountManager { @Override public void startAccountCreation(final Activity activity) { + + // skipping AuthenticatorActivity redirection when user is on Launcher or FirstRun Activity + if (activity instanceof LauncherActivity || activity instanceof FirstRunActivity) return; + Intent intent = new Intent(context, AuthenticatorActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); diff --git a/app/src/main/java/com/nextcloud/client/appinfo/AppInfo.kt b/app/src/main/java/com/nextcloud/client/appinfo/AppInfo.kt index 3abb1a7..acc55a6 100644 --- a/app/src/main/java/com/nextcloud/client/appinfo/AppInfo.kt +++ b/app/src/main/java/com/nextcloud/client/appinfo/AppInfo.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.appinfo diff --git a/app/src/main/java/com/nextcloud/client/appinfo/AppInfoImpl.kt b/app/src/main/java/com/nextcloud/client/appinfo/AppInfoImpl.kt index 1f36ae6..9868e0a 100644 --- a/app/src/main/java/com/nextcloud/client/appinfo/AppInfoImpl.kt +++ b/app/src/main/java/com/nextcloud/client/appinfo/AppInfoImpl.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.appinfo @@ -16,17 +16,11 @@ class AppInfoImpl : AppInfo { override val versionCode: Int = BuildConfig.VERSION_CODE override val isDebugBuild: Boolean = BuildConfig.DEBUG - override fun getAppVersion(context: Context): String { - return try { - val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) - if (pInfo != null) { - pInfo.versionName - } else { - "n/a" - } - } catch (e: PackageManager.NameNotFoundException) { - Log_OC.e(this, "Trying to get packageName", e.cause) - "n/a" - } + override fun getAppVersion(context: Context): String = try { + val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) + packageInfo.versionName ?: "n/a" + } catch (e: PackageManager.NameNotFoundException) { + Log_OC.e(this, "Trying to get packageName", e.cause) + "n/a" } } diff --git a/app/src/main/java/com/nextcloud/client/appinfo/AppInfoModule.kt b/app/src/main/java/com/nextcloud/client/appinfo/AppInfoModule.kt index 4fd0ff0..5e32efb 100644 --- a/app/src/main/java/com/nextcloud/client/appinfo/AppInfoModule.kt +++ b/app/src/main/java/com/nextcloud/client/appinfo/AppInfoModule.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.appinfo @@ -12,7 +12,5 @@ import dagger.Provides @Module class AppInfoModule { @Provides - fun appInfo(): AppInfo { - return AppInfoImpl() - } + fun appInfo(): AppInfo = AppInfoImpl() } diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt new file mode 100644 index 0000000..a1931bf --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantScreen.kt @@ -0,0 +1,334 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.nextcloud.client.assistant + +import android.app.Activity +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FabPosition +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.pullToRefresh +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.nextcloud.client.assistant.component.AddTaskAlertDialog +import com.nextcloud.client.assistant.extensions.getInputTitle +import com.nextcloud.client.assistant.model.ScreenOverlayState +import com.nextcloud.client.assistant.model.ScreenState +import com.nextcloud.client.assistant.repository.local.MockAssistantLocalRepository +import com.nextcloud.client.assistant.repository.remote.MockAssistantRemoteRepository +import com.nextcloud.client.assistant.task.TaskView +import com.nextcloud.client.assistant.taskTypes.TaskTypesRow +import com.nextcloud.ui.composeActivity.ComposeActivity +import com.nextcloud.ui.composeComponents.alertDialog.SimpleAlertDialog +import com.nextcloud.ui.composeComponents.bottomSheet.MoreActionsBottomSheet +import com.owncloud.android.R +import com.owncloud.android.lib.resources.assistant.v2.model.Task +import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData +import com.owncloud.android.lib.resources.status.OCCapability +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +private const val PULL_TO_REFRESH_DELAY = 1500L + +@Suppress("LongMethod") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AssistantScreen(viewModel: AssistantViewModel, capability: OCCapability, activity: Activity) { + val messageId by viewModel.snackbarMessageId.collectAsState() + val screenOverlayState by viewModel.screenOverlayState.collectAsState() + + val selectedTaskType by viewModel.selectedTaskType.collectAsState() + val filteredTaskList by viewModel.filteredTaskList.collectAsState() + val screenState by viewModel.screenState.collectAsState() + val taskTypes by viewModel.taskTypes.collectAsState() + val scope = rememberCoroutineScope() + val pullRefreshState = rememberPullToRefreshState() + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(messageId) { + messageId?.let { + snackbarHostState.showSnackbar(activity.getString(it)) + viewModel.updateSnackbarMessage(null) + } + } + + LaunchedEffect(Unit) { + viewModel.startTaskListPolling() + } + + DisposableEffect(Unit) { + onDispose { + viewModel.stopTaskListPolling() + } + } + + Scaffold( + modifier = Modifier.pullToRefresh( + false, + pullRefreshState, + onRefresh = { + scope.launch { + delay(PULL_TO_REFRESH_DELAY) + viewModel.fetchTaskList() + } + } + ), + topBar = { + taskTypes?.let { + TaskTypesRow(selectedTaskType, data = it) { task -> + viewModel.selectTaskType(task) + } + } + }, + floatingActionButton = { + if (!taskTypes.isNullOrEmpty()) { + AddTaskButton( + selectedTaskType, + viewModel + ) + } + }, + floatingActionButtonPosition = FabPosition.EndOverlay, + snackbarHost = { + SnackbarHost(snackbarHostState) + } + ) { paddingValues -> + when (screenState) { + is ScreenState.EmptyContent -> { + val state = (screenState as ScreenState.EmptyContent) + EmptyContent( + paddingValues, + state.iconId, + state.descriptionId + ) + } + + ScreenState.Content -> { + AssistantContent( + paddingValues, + filteredTaskList ?: listOf(), + viewModel, + capability + ) + } + + else -> EmptyContent( + paddingValues, + R.drawable.spinner_inner, + R.string.assistant_screen_loading + ) + } + + LinearProgressIndicator( + progress = { pullRefreshState.distanceFraction }, + modifier = Modifier.fillMaxWidth() + ) + + OverlayState(screenOverlayState, activity, viewModel) + } +} + +@Composable +private fun AddTaskButton(selectedTaskType: TaskTypeData?, viewModel: AssistantViewModel) { + FloatingActionButton( + onClick = { + selectedTaskType?.let { + val newState = ScreenOverlayState.AddTask(it, "") + viewModel.updateTaskListScreenState(newState) + } + } + ) { + Icon(Icons.Filled.Add, "Add Task Icon") + } +} + +@Suppress("LongMethod") +@Composable +private fun OverlayState(state: ScreenOverlayState?, activity: Activity, viewModel: AssistantViewModel) { + when (state) { + is ScreenOverlayState.AddTask -> { + AddTaskAlertDialog( + title = state.taskType.name, + description = state.taskType.description, + defaultInput = state.input, + addTask = { input -> + state.taskType.let { taskType -> + viewModel.createTask(input = input, taskType = taskType) + } + }, + dismiss = { + viewModel.updateTaskListScreenState(null) + } + ) + } + + is ScreenOverlayState.DeleteTask -> { + SimpleAlertDialog( + title = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_title), + description = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_description), + dismiss = { viewModel.updateTaskListScreenState(null) }, + onComplete = { viewModel.deleteTask(state.id) } + ) + } + + is ScreenOverlayState.TaskActions -> { + val actions = state.getActions(activity, onEditCompleted = { addTask -> + viewModel.updateTaskListScreenState(addTask) + }, onDeleteCompleted = { deleteTask -> + viewModel.updateTaskListScreenState(deleteTask) + }) + + MoreActionsBottomSheet( + title = state.task.getInputTitle(), + actions = actions, + dismiss = { viewModel.updateTaskListScreenState(null) } + ) + } + + else -> Unit + } +} + +@Composable +private fun AssistantContent( + paddingValues: PaddingValues, + taskList: List, + viewModel: AssistantViewModel, + capability: OCCapability +) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(12.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + items(taskList, key = { it.id }) { task -> + TaskView( + task, + capability, + showTaskActions = { + val newState = ScreenOverlayState.TaskActions(task) + viewModel.updateTaskListScreenState(newState) + } + ) + Spacer(modifier = Modifier.height(8.dp)) + } + } +} + +@Composable +private fun EmptyContent(paddingValues: PaddingValues, iconId: Int?, descriptionId: Int) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + iconId?.let { + Image( + painter = painterResource(id = iconId), + modifier = Modifier.size(32.dp), + colorFilter = ColorFilter.tint(color = colorResource(R.color.text_color)), + contentDescription = "empty content icon" + ) + + Spacer(modifier = Modifier.height(8.dp)) + } + + Text( + text = stringResource(descriptionId), + fontSize = 18.sp, + textAlign = TextAlign.Center, + color = colorResource(R.color.text_color) + ) + } +} + +@Suppress("MagicNumber") +@Composable +@Preview +private fun AssistantScreenPreview() { + MaterialTheme( + content = { + AssistantScreen( + viewModel = getMockViewModel(false), + activity = ComposeActivity(), + capability = OCCapability().apply { + versionMayor = 30 + } + ) + } + ) +} + +@Suppress("MagicNumber") +@Composable +@Preview +private fun AssistantEmptyScreenPreview() { + MaterialTheme( + content = { + AssistantScreen( + viewModel = getMockViewModel(true), + activity = ComposeActivity(), + capability = OCCapability().apply { + versionMayor = 30 + } + ) + } + ) +} + +private fun getMockViewModel(giveEmptyTasks: Boolean): AssistantViewModel { + val mockLocalRepository = MockAssistantLocalRepository() + val mockRemoteRepository = MockAssistantRemoteRepository(giveEmptyTasks) + return AssistantViewModel( + accountName = "test:localhost", + remoteRepository = mockRemoteRepository, + localRepository = mockLocalRepository + ) +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt index ba27fa0..6f64bb6 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AssistantViewModel.kt @@ -1,66 +1,110 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2024 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.assistant -import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.nextcloud.client.assistant.repository.AssistantRepositoryType +import com.nextcloud.client.assistant.model.ScreenOverlayState +import com.nextcloud.client.assistant.model.ScreenState +import com.nextcloud.client.assistant.repository.local.AssistantLocalRepository +import com.nextcloud.client.assistant.repository.remote.AssistantRemoteRepository import com.owncloud.android.R -import com.owncloud.android.lib.resources.assistant.model.Task -import com.owncloud.android.lib.resources.assistant.model.TaskType +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.assistant.v2.model.Task +import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import java.lang.ref.WeakReference class AssistantViewModel( - private val repository: AssistantRepositoryType, - private val context: WeakReference + private val accountName: String, + private val remoteRepository: AssistantRemoteRepository, + private val localRepository: AssistantLocalRepository ) : ViewModel() { - sealed class State { - data object Idle : State() - data object Loading : State() - data class Error(val messageId: Int) : State() - data class TaskCreated(val messageId: Int) : State() - data class TaskDeleted(val messageId: Int) : State() + companion object { + private const val TAG = "AssistantViewModel" + private const val TASK_LIST_POLLING_INTERVAL_MS = 15_000L } - private val _state = MutableStateFlow(State.Loading) - val state: StateFlow = _state + private val _screenState = MutableStateFlow(null) + val screenState: StateFlow = _screenState - private val _selectedTaskType = MutableStateFlow(null) - val selectedTaskType: StateFlow = _selectedTaskType + private val _screenOverlayState = MutableStateFlow(null) + val screenOverlayState: StateFlow = _screenOverlayState - private val _taskTypes = MutableStateFlow?>(null) - val taskTypes: StateFlow?> = _taskTypes + private val _snackbarMessageId = MutableStateFlow(null) + val snackbarMessageId: StateFlow = _snackbarMessageId - private var _taskList: List? = null + private val _selectedTaskType = MutableStateFlow(null) + val selectedTaskType: StateFlow = _selectedTaskType + + private val _taskTypes = MutableStateFlow?>(null) + val taskTypes: StateFlow?> = _taskTypes + + private var taskList: List? = null private val _filteredTaskList = MutableStateFlow?>(null) val filteredTaskList: StateFlow?> = _filteredTaskList + private var taskPollingJob: Job? = null + init { fetchTaskTypes() - fetchTaskList() + } + + // region task polling + fun startTaskListPolling() { + stopTaskListPolling() + + taskPollingJob = viewModelScope.launch(Dispatchers.IO) { + try { + while (isActive) { + Log_OC.d(TAG, "Polling task list...") + fetchTaskListSuspending() + delay(TASK_LIST_POLLING_INTERVAL_MS) + } + } finally { + Log_OC.d(TAG, "Polling coroutine cancelled") + } + } + } + + fun stopTaskListPolling() { + taskPollingJob?.cancel() + taskPollingJob = null + } + // endregion + + private suspend fun fetchTaskListSuspending() { + val cachedTasks = localRepository.getCachedTasks(accountName) + if (cachedTasks.isNotEmpty()) { + _filteredTaskList.value = cachedTasks.sortedByDescending { it.id } + } + + val taskType = _selectedTaskType.value?.id ?: return + val result = remoteRepository.getTaskList(taskType) + if (result != null) { + taskList = result + _filteredTaskList.value = taskList?.sortedByDescending { it.id } + localRepository.cacheTasks(result, accountName) + } } @Suppress("MagicNumber") - fun createTask( - input: String, - type: String - ) { + fun createTask(input: String, taskType: TaskTypeData) { viewModelScope.launch(Dispatchers.IO) { - val result = repository.createTask(input, type) + val result = remoteRepository.createTask(input, taskType) val messageId = if (result.isSuccess) { R.string.assistant_screen_task_create_success_message @@ -68,69 +112,83 @@ class AssistantViewModel( R.string.assistant_screen_task_create_fail_message } - _state.update { - State.TaskCreated(messageId) - } + updateSnackbarMessage(messageId) delay(2000L) fetchTaskList() } } - fun selectTaskType(task: TaskType) { + fun selectTaskType(task: TaskTypeData) { _selectedTaskType.update { - filterTaskList(task.id) task } + + fetchTaskList() } private fun fetchTaskTypes() { viewModelScope.launch(Dispatchers.IO) { - val allTaskType = context.get()?.getString(R.string.assistant_screen_all_task_type) - val excludedIds = listOf("OCA\\ContextChat\\TextProcessing\\ContextChatTaskType") - val result = arrayListOf(TaskType(null, allTaskType, null)) - val taskTypesResult = repository.getTaskTypes() - - if (taskTypesResult.isSuccess) { - val excludedTaskTypes = taskTypesResult.resultData.types.filter { item -> item.id !in excludedIds } - result.addAll(excludedTaskTypes) - _taskTypes.update { - result.toList() - } - - selectTaskType(result.first()) - } else { - _state.update { - State.Error(R.string.assistant_screen_task_types_error_state_message) + val taskTypesResult = remoteRepository.getTaskTypes() + if (taskTypesResult == null || taskTypesResult.isEmpty()) { + _screenState.update { + ScreenState.emptyTaskTypes() } + return@launch } + + _taskTypes.update { + taskTypesResult + } + + selectTaskType(taskTypesResult.first()) } } - fun fetchTaskList(appId: String = "assistant", onCompleted: () -> Unit = {}) { + fun fetchTaskList() { viewModelScope.launch(Dispatchers.IO) { - val result = repository.getTaskList(appId) - if (result.isSuccess) { - _taskList = result.resultData.tasks + // Try cached data first + val cachedTasks = localRepository.getCachedTasks(accountName) + if (cachedTasks.isNotEmpty()) { + _filteredTaskList.update { + cachedTasks.sortedByDescending { it.id } + } + updateTaskListScreenState() + } - filterTaskList(_selectedTaskType.value?.id) - - _state.update { - State.Idle + val taskType = _selectedTaskType.value?.id ?: return@launch + val result = remoteRepository.getTaskList(taskType) + if (result != null) { + taskList = result + _filteredTaskList.update { + taskList?.sortedByDescending { task -> + task.id + } } - onCompleted() + localRepository.cacheTasks(result, accountName) + updateSnackbarMessage(null) } else { - _state.update { - State.Error(R.string.assistant_screen_task_list_error_state_message) - } + updateSnackbarMessage(R.string.assistant_screen_task_list_error_state_message) + } + + updateTaskListScreenState() + } + } + + private fun updateTaskListScreenState() { + _screenState.update { + if (_filteredTaskList.value?.isEmpty() == true) { + ScreenState.emptyTaskList() + } else { + ScreenState.Content } } } fun deleteTask(id: Long) { viewModelScope.launch(Dispatchers.IO) { - val result = repository.deleteTask(id) + val result = remoteRepository.deleteTask(id) val messageId = if (result.isSuccess) { R.string.assistant_screen_task_delete_success_message @@ -138,37 +196,24 @@ class AssistantViewModel( R.string.assistant_screen_task_delete_fail_message } - _state.update { - State.TaskDeleted(messageId) - } + updateSnackbarMessage(messageId) if (result.isSuccess) { removeTaskFromList(id) + localRepository.deleteTask(id, accountName) } } } - fun resetState() { - _state.update { - State.Idle + fun updateSnackbarMessage(value: Int?) { + _snackbarMessageId.update { + value } } - private fun filterTaskList(taskTypeId: String?) { - if (taskTypeId == null) { - _filteredTaskList.update { - _taskList - } - } else { - _filteredTaskList.update { - _taskList?.filter { it.type == taskTypeId } - } - } - - _filteredTaskList.update { - it?.sortedByDescending { task -> - task.id - } + fun updateTaskListScreenState(value: ScreenOverlayState?) { + _screenOverlayState.update { + value } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/AsssistantScreen.kt b/app/src/main/java/com/nextcloud/client/assistant/AsssistantScreen.kt index 4dd567c..e19c6de 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/AsssistantScreen.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/AsssistantScreen.kt @@ -1,14 +1,13 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2024 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.assistant import android.app.Activity -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -25,180 +24,250 @@ import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.pulltorefresh.PullToRefreshState +import androidx.compose.material3.pulltorefresh.pullToRefresh import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.nextcloud.client.assistant.component.AddTaskAlertDialog import com.nextcloud.client.assistant.component.CenterText -import com.nextcloud.client.assistant.taskTypes.TaskTypesRow -import com.nextcloud.client.assistant.task.TaskView +import com.nextcloud.client.assistant.extensions.getInputTitle +import com.nextcloud.client.assistant.model.ScreenOverlayState +import com.nextcloud.client.assistant.model.ScreenState import com.nextcloud.client.assistant.repository.AssistantMockRepository +import com.nextcloud.client.assistant.task.TaskView +import com.nextcloud.client.assistant.taskTypes.TaskTypesRow import com.nextcloud.ui.composeActivity.ComposeActivity import com.nextcloud.ui.composeComponents.alertDialog.SimpleAlertDialog +import com.nextcloud.ui.composeComponents.bottomSheet.MoreActionsBottomSheet import com.owncloud.android.R -import com.owncloud.android.lib.resources.assistant.model.Task -import com.owncloud.android.lib.resources.assistant.model.TaskType +import com.owncloud.android.lib.resources.assistant.v2.model.Task +import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData +import com.owncloud.android.lib.resources.status.OCCapability import com.owncloud.android.utils.DisplayUtils import kotlinx.coroutines.delay -import java.lang.ref.WeakReference +import kotlinx.coroutines.launch @Suppress("LongMethod") @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AssistantScreen(viewModel: AssistantViewModel, activity: Activity) { - val state by viewModel.state.collectAsState() +fun AssistantScreen(viewModel: AssistantViewModel, capability: OCCapability, activity: Activity) { + val messageId by viewModel.snackbarMessageId.collectAsState() + val screenOverlayState by viewModel.screenOverlayState.collectAsState() + val selectedTaskType by viewModel.selectedTaskType.collectAsState() val filteredTaskList by viewModel.filteredTaskList.collectAsState() + val screenState by viewModel.screenState.collectAsState() val taskTypes by viewModel.taskTypes.collectAsState() - var showAddTaskAlertDialog by remember { mutableStateOf(false) } - var showDeleteTaskAlertDialog by remember { mutableStateOf(false) } - var taskIdToDeleted: Long? by remember { - mutableStateOf(null) - } + val scope = rememberCoroutineScope() val pullRefreshState = rememberPullToRefreshState() @Suppress("MagicNumber") - if (pullRefreshState.isRefreshing) { - LaunchedEffect(true) { - delay(1500) - viewModel.fetchTaskList(onCompleted = { - pullRefreshState.endRefresh() - }) - } + Box( + modifier = Modifier.pullToRefresh( + screenState == ScreenState.Refreshing, + pullRefreshState, + onRefresh = { + scope.launch { + delay(1500) + viewModel.fetchTaskList() + } + } + ) + ) { + ShowScreenState(screenState, selectedTaskType, taskTypes, viewModel, filteredTaskList, capability) + + ShowLinearProgressIndicator(screenState, pullRefreshState) + + AddFloatingActionButton( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp), + selectedTaskType, + viewModel + ) } - Box(Modifier.nestedScroll(pullRefreshState.nestedScrollConnection)) { - if (state == AssistantViewModel.State.Loading || pullRefreshState.isRefreshing) { + showSnackBarMessage(messageId, activity, viewModel) + ShowOverlayState(screenOverlayState, activity, viewModel) +} + +@Composable +private fun ShowScreenState( + screenState: ScreenState?, + selectedTaskType: TaskTypeData?, + taskTypes: List?, + viewModel: AssistantViewModel, + filteredTaskList: List?, + capability: OCCapability +) { + when (screenState) { + ScreenState.Refreshing -> { CenterText(text = stringResource(id = R.string.assistant_screen_loading)) - } else { - if (filteredTaskList.isNullOrEmpty()) { - EmptyTaskList(selectedTaskType, taskTypes, viewModel) - } else { - AssistantContent( - filteredTaskList!!, - taskTypes, - selectedTaskType, - viewModel, - showDeleteTaskAlertDialog = { taskId -> - taskIdToDeleted = taskId - showDeleteTaskAlertDialog = true - } - ) - } } - if (pullRefreshState.isRefreshing) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) - } else { - LinearProgressIndicator(progress = { pullRefreshState.progress }, modifier = Modifier.fillMaxWidth()) + ScreenState.EmptyContent -> { + EmptyTaskList(selectedTaskType, taskTypes, viewModel) } - if (selectedTaskType?.name != stringResource(id = R.string.assistant_screen_all_task_type)) { - FloatingActionButton( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(16.dp), - onClick = { - showAddTaskAlertDialog = true - } - ) { - Icon(Icons.Filled.Add, "Add Task Icon") - } - } - } - - ScreenState(state, activity, viewModel) - - if (showDeleteTaskAlertDialog) { - taskIdToDeleted?.let { id -> - SimpleAlertDialog( - title = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_title), - description = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_description), - dismiss = { showDeleteTaskAlertDialog = false }, - onComplete = { viewModel.deleteTask(id) } + ScreenState.Content -> { + AssistantContent( + filteredTaskList ?: listOf(), + taskTypes, + selectedTaskType, + viewModel, + capability ) } - } - if (showAddTaskAlertDialog) { - selectedTaskType?.let { taskType -> - AddTaskAlertDialog( - title = taskType.name, - description = taskType.description, - addTask = { input -> - taskType.id?.let { - viewModel.createTask(input = input, type = it) - } - }, - dismiss = { - showAddTaskAlertDialog = false - } - ) - } + null -> Unit + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ShowLinearProgressIndicator(screenState: ScreenState?, pullToRefreshState: PullToRefreshState) { + if (screenState == ScreenState.Refreshing) { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } else { + LinearProgressIndicator( + progress = { pullToRefreshState.distanceFraction }, + modifier = Modifier.fillMaxWidth() + ) } } @Composable -private fun ScreenState( - state: AssistantViewModel.State, - activity: Activity, +private fun AddFloatingActionButton( + modifier: Modifier, + selectedTaskType: TaskTypeData?, viewModel: AssistantViewModel ) { - val messageId: Int? = when (state) { - is AssistantViewModel.State.Error -> { - state.messageId - } - - is AssistantViewModel.State.TaskCreated -> { - state.messageId - } - - is AssistantViewModel.State.TaskDeleted -> { - state.messageId - } - - else -> { - null + FloatingActionButton( + modifier = modifier, + onClick = { + selectedTaskType?.let { + val newState = ScreenOverlayState.AddTask(it, "") + viewModel.updateScreenState(newState) + } } + ) { + Icon(Icons.Filled.Add, "Add Task Icon") } +} +private fun showSnackBarMessage(messageId: Int?, activity: Activity, viewModel: AssistantViewModel) { messageId?.let { DisplayUtils.showSnackMessage( activity, - stringResource(id = messageId) + activity.getString(it) ) - viewModel.resetState() + viewModel.updateSnackbarMessage(null) + } +} + +@Suppress("LongMethod") +@Composable +private fun ShowOverlayState(state: ScreenOverlayState?, activity: Activity, viewModel: AssistantViewModel) { + when (state) { + is ScreenOverlayState.AddTask -> { + AddTaskAlertDialog( + title = state.taskType.name, + description = state.taskType.description, + defaultInput = state.input, + addTask = { input -> + state.taskType.let { taskType -> + viewModel.createTask(input = input, taskType = taskType) + } + }, + dismiss = { + viewModel.updateScreenState(null) + } + ) + } + + is ScreenOverlayState.DeleteTask -> { + SimpleAlertDialog( + title = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_title), + description = stringResource(id = R.string.assistant_screen_delete_task_alert_dialog_description), + dismiss = { viewModel.updateScreenState(null) }, + onComplete = { viewModel.deleteTask(state.id) } + ) + } + + is ScreenOverlayState.TaskActions -> { + val actions = state.getActions(activity, onEditCompleted = { addTask -> + viewModel.updateScreenState(addTask) + }, onDeleteCompleted = { deleteTask -> + viewModel.updateScreenState(deleteTask) + }) + + MoreActionsBottomSheet( + title = state.task.getInputTitle(), + actions = actions, + dismiss = { viewModel.updateScreenState(null) } + ) + } + + else -> Unit } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun AssistantContent( taskList: List, - taskTypes: List?, - selectedTaskType: TaskType?, + taskTypes: List?, + selectedTaskType: TaskTypeData?, viewModel: AssistantViewModel, - showDeleteTaskAlertDialog: (Long) -> Unit + capability: OCCapability ) { - LazyColumn( + Column(modifier = Modifier.fillMaxSize()) { + taskTypes?.let { + TaskTypesRow(selectedTaskType, data = taskTypes) { task -> + viewModel.selectTaskType(task) + } + } + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(12.dp) + ) { + items(taskList) { task -> + TaskView( + task, + capability, + showTaskActions = { + val newState = ScreenOverlayState.TaskActions(task) + viewModel.updateScreenState(newState) + } + ) + Spacer(modifier = Modifier.height(8.dp)) + } + } + } +} + +@Composable +private fun EmptyTaskList( + selectedTaskType: TaskTypeData?, + taskTypes: List?, + viewModel: AssistantViewModel +) { + Column( modifier = Modifier .fillMaxSize() .padding(16.dp) ) { - stickyHeader { + taskTypes?.let { TaskTypesRow(selectedTaskType, data = taskTypes) { task -> viewModel.selectTaskType(task) } @@ -206,39 +275,15 @@ private fun AssistantContent( Spacer(modifier = Modifier.height(8.dp)) } - items(taskList) { task -> - TaskView(task, showDeleteTaskAlertDialog = { showDeleteTaskAlertDialog(task.id) }) - Spacer(modifier = Modifier.height(8.dp)) - } - } -} - -@Composable -private fun EmptyTaskList(selectedTaskType: TaskType?, taskTypes: List?, viewModel: AssistantViewModel) { - val text = if (selectedTaskType?.name == stringResource(id = R.string.assistant_screen_all_task_type)) { - stringResource(id = R.string.assistant_screen_no_task_available_for_all_task_filter_text) - } else { - stringResource( - id = R.string.assistant_screen_no_task_available_text, - selectedTaskType?.name ?: "" + CenterText( + text = stringResource( + id = R.string.assistant_screen_create_a_new_task_from_bottom_right_text + ) ) } - - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp) - ) { - TaskTypesRow(selectedTaskType, data = taskTypes) { task -> - viewModel.selectTaskType(task) - } - - Spacer(modifier = Modifier.height(8.dp)) - - CenterText(text = text) - } } +@Suppress("MagicNumber") @Composable @Preview private fun AssistantScreenPreview() { @@ -246,16 +291,17 @@ private fun AssistantScreenPreview() { MaterialTheme( content = { AssistantScreen( - viewModel = AssistantViewModel( - repository = mockRepository, - context = WeakReference(LocalContext.current) - ), - activity = ComposeActivity() + viewModel = AssistantViewModel(repository = mockRepository), + activity = ComposeActivity(), + capability = OCCapability().apply { + versionMayor = 30 + } ) } ) } +@Suppress("MagicNumber") @Composable @Preview private fun AssistantEmptyScreenPreview() { @@ -263,11 +309,11 @@ private fun AssistantEmptyScreenPreview() { MaterialTheme( content = { AssistantScreen( - viewModel = AssistantViewModel( - repository = mockRepository, - context = WeakReference(LocalContext.current) - ), - activity = ComposeActivity() + viewModel = AssistantViewModel(repository = mockRepository), + activity = ComposeActivity(), + capability = OCCapability().apply { + versionMayor = 30 + } ) } ) diff --git a/app/src/main/java/com/nextcloud/client/assistant/component/AddTaskAlertDialog.kt b/app/src/main/java/com/nextcloud/client/assistant/component/AddTaskAlertDialog.kt index b97f425..e9ccbb4 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/component/AddTaskAlertDialog.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/component/AddTaskAlertDialog.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2024 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.assistant.component @@ -22,13 +22,19 @@ import com.nextcloud.ui.composeComponents.alertDialog.SimpleAlertDialog import com.owncloud.android.R @Composable -fun AddTaskAlertDialog(title: String?, description: String?, addTask: (String) -> Unit, dismiss: () -> Unit) { +fun AddTaskAlertDialog( + title: String, + description: String?, + defaultInput: String = "", + addTask: (String) -> Unit, + dismiss: () -> Unit +) { var input by remember { - mutableStateOf("") + mutableStateOf(defaultInput) } SimpleAlertDialog( - title = title ?: "", + title = title, description = description ?: "", dismiss = { dismiss() }, onComplete = { diff --git a/app/src/main/java/com/nextcloud/client/assistant/component/CenterText.kt b/app/src/main/java/com/nextcloud/client/assistant/component/CenterText.kt index 2f87f96..cf3cefb 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/component/CenterText.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/component/CenterText.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2024 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.assistant.component @@ -13,8 +13,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.sp +import com.owncloud.android.R @Composable fun CenterText(text: String) { @@ -22,7 +24,8 @@ fun CenterText(text: String) { Text( text = text, fontSize = 18.sp, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, + color = colorResource(R.color.text_color) ) } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/extensions/TaskExtensions.kt b/app/src/main/java/com/nextcloud/client/assistant/extensions/TaskExtensions.kt index f2ff257..cef6c95 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/extensions/TaskExtensions.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/extensions/TaskExtensions.kt @@ -3,40 +3,130 @@ * * SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud * contributors - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-License-Identifier: MIT */ package com.nextcloud.client.assistant.extensions +import android.content.Context +import com.nextcloud.utils.date.DateFormatPattern +import com.nextcloud.utils.date.DateFormatter import com.owncloud.android.R -import com.owncloud.android.lib.resources.assistant.model.Task +import com.owncloud.android.lib.resources.assistant.v2.model.Task +import com.owncloud.android.lib.resources.status.NextcloudVersion +import com.owncloud.android.lib.resources.status.OCCapability +import java.util.concurrent.TimeUnit + +fun Task.getInputAndOutput(): String { + val inputText = input?.input ?: "" + val outputText = output?.output ?: "" + + return "$inputText\n\n$outputText" +} + +fun Task.getInput(): String? = input?.input @Suppress("MagicNumber") -fun Task.statusData(): Pair { - return when (status) { - 0L -> { - Pair(R.drawable.ic_unknown, R.string.assistant_screen_unknown_task_status_text) - } - 1L -> { - Pair(R.drawable.ic_clock, R.string.assistant_screen_scheduled_task_status_text) - } - 2L -> { - Pair(R.drawable.ic_modification_desc, R.string.assistant_screen_running_task_text) - } - 3L -> { - Pair(R.drawable.ic_info, R.string.assistant_screen_successful_task_text) - } - 4L -> { - Pair(R.drawable.image_fail, R.string.assistant_screen_failed_task_text) - } - else -> { - Pair(R.drawable.ic_unknown, R.string.assistant_screen_unknown_task_status_text) - } +fun Task.getInputTitle(): String { + val maxTitleLength = 20 + val title = getInput() ?: "" + + return if (title.length > maxTitleLength) { + title.take(maxTitleLength) + "..." + } else { + title } } -// TODO add -fun Task.completionDateRepresentation(): String { - return completionExpectedAt ?: "TODO IMPLEMENT IT" +fun Task.getStatusIcon(capability: OCCapability): Int = + if (capability.version.isNewerOrEqual(NextcloudVersion.nextcloud_30)) { + getStatusIconV2() + } else { + getStatusIconV1() + } + +private fun Task.getStatusIconV1(): Int = when (status) { + "0" -> { + R.drawable.ic_unknown + } + "1" -> { + R.drawable.ic_clock + } + "2" -> { + R.drawable.ic_modification_desc + } + "3" -> { + R.drawable.ic_check_circle_outline + } + "4" -> { + R.drawable.image_fail + } + else -> { + R.drawable.ic_unknown + } +} + +private fun Task.getStatusIconV2(): Int = when (status) { + "STATUS_UNKNOWN" -> { + R.drawable.ic_unknown + } + "STATUS_SCHEDULED" -> { + R.drawable.ic_clock + } + "STATUS_RUNNING" -> { + R.drawable.ic_modification_desc + } + "STATUS_SUCCESSFUL" -> { + R.drawable.ic_check_circle_outline + } + "STATUS_FAILED" -> { + R.drawable.image_fail + } + else -> { + R.drawable.ic_unknown + } +} + +@Suppress("MagicNumber") +fun Task.getModifiedAtRepresentation(context: Context): String? { + if (lastUpdated == null) { + return null + } + + val modifiedAt = lastUpdated!!.toLong() + val currentTime = System.currentTimeMillis() / 1000 + val timeDifference = (currentTime - modifiedAt).toInt() + val timeDifferenceInMinutes = (timeDifference / 60) + val timeDifferenceInHours = (timeDifference / 3600) + + return when { + timeDifference < 0 -> { + context.getString(R.string.common_now) + } + + timeDifference < TimeUnit.MINUTES.toSeconds(1) -> { + context.resources.getQuantityString(R.plurals.time_seconds_ago, timeDifference, timeDifference) + } + + timeDifference < TimeUnit.HOURS.toSeconds(1) -> { + context.resources.getQuantityString( + R.plurals.time_minutes_ago, + timeDifferenceInMinutes, + timeDifferenceInMinutes + ) + } + + timeDifference < TimeUnit.DAYS.toSeconds(1) -> { + context.resources.getQuantityString( + R.plurals.time_hours_ago, + timeDifferenceInHours, + timeDifferenceInHours + ) + } + + else -> { + DateFormatter.timestampToDateRepresentation(modifiedAt, DateFormatPattern.MonthWithDate) + } + } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/model/ScreenOverlayState.kt b/app/src/main/java/com/nextcloud/client/assistant/model/ScreenOverlayState.kt new file mode 100644 index 0000000..02ceb33 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/model/ScreenOverlayState.kt @@ -0,0 +1,79 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.assistant.model + +import android.app.Activity +import com.nextcloud.client.assistant.extensions.getInput +import com.nextcloud.client.assistant.extensions.getInputAndOutput +import com.nextcloud.utils.extensions.showShareIntent +import com.owncloud.android.R +import com.owncloud.android.lib.resources.assistant.v2.model.Task +import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData +import com.owncloud.android.utils.ClipboardUtil + +sealed class ScreenOverlayState { + data class DeleteTask(val id: Long) : ScreenOverlayState() + data class AddTask(val taskType: TaskTypeData, val input: String) : ScreenOverlayState() + data class TaskActions(val task: Task) : ScreenOverlayState() { + private fun getInputAndOutput(): String = task.getInputAndOutput() + private fun getInput(): String? = task.getInput() + + private fun getCopyToClipboardAction(activity: Activity): Triple Unit> = Triple( + R.drawable.ic_content_copy, + R.string.common_copy + ) { + ClipboardUtil.copyToClipboard(activity, getInputAndOutput(), showToast = false) + } + + private fun getShareAction(activity: Activity): Triple Unit> = Triple( + R.drawable.ic_share, + R.string.common_share + ) { + activity.showShareIntent(getInputAndOutput()) + } + + private fun getEditAction(activity: Activity, onComplete: (AddTask) -> Unit): Triple Unit> = + Triple( + R.drawable.ic_edit, + R.string.action_edit + ) { + val taskType = TaskTypeData( + task.type, + activity.getString(R.string.assistant_screen_add_task_alert_dialog_title), + null, + emptyMap(), + emptyMap() + ) + val newState = AddTask(taskType, getInput() ?: "") + onComplete(newState) + } + + private fun getDeleteAction(onComplete: (DeleteTask) -> Unit): Triple Unit> = Triple( + R.drawable.ic_delete, + R.string.assistant_screen_task_more_actions_bottom_sheet_delete_action + ) { + val newState = DeleteTask(task.id) + onComplete(newState) + } + + fun getActions( + activity: Activity, + onEditCompleted: (AddTask) -> Unit, + onDeleteCompleted: (DeleteTask) -> Unit + ): List Unit>> = listOf( + getShareAction(activity), + getCopyToClipboardAction(activity), + getEditAction(activity, onComplete = { + onEditCompleted(it) + }), + getDeleteAction(onComplete = { + onDeleteCompleted(it) + }) + ) + } +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/model/ScreenState.kt b/app/src/main/java/com/nextcloud/client/assistant/model/ScreenState.kt new file mode 100644 index 0000000..3c2ba4d --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/model/ScreenState.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.assistant.model + +import com.owncloud.android.R + +sealed class ScreenState { + data object Loading : ScreenState() + + data object Content : ScreenState() + + data class EmptyContent(val iconId: Int?, val descriptionId: Int) : ScreenState() + + companion object { + fun emptyTaskTypes(): ScreenState = EmptyContent( + descriptionId = R.string.assistant_screen_task_list_empty_warning, + iconId = null + ) + + fun emptyTaskList(): ScreenState = EmptyContent( + descriptionId = R.string.assistant_screen_create_a_new_task_from_bottom_right_text, + iconId = null + ) + } +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/AssistantMockRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/AssistantMockRepository.kt index 04efe98..b130937 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/AssistantMockRepository.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/AssistantMockRepository.kt @@ -1,128 +1,68 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2024 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.assistant.repository import com.nextcloud.utils.extensions.getRandomString import com.owncloud.android.lib.common.operations.RemoteOperationResult -import com.owncloud.android.lib.resources.assistant.model.Task -import com.owncloud.android.lib.resources.assistant.model.TaskList -import com.owncloud.android.lib.resources.assistant.model.TaskType -import com.owncloud.android.lib.resources.assistant.model.TaskTypes +import com.owncloud.android.lib.resources.assistant.v2.model.Shape +import com.owncloud.android.lib.resources.assistant.v2.model.Task +import com.owncloud.android.lib.resources.assistant.v2.model.TaskInput +import com.owncloud.android.lib.resources.assistant.v2.model.TaskOutput +import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData @Suppress("MagicNumber") class AssistantMockRepository(private val giveEmptyTasks: Boolean = false) : AssistantRepositoryType { - override fun getTaskTypes(): RemoteOperationResult { - return RemoteOperationResult(RemoteOperationResult.ResultCode.OK).apply { - resultData = TaskTypes( - listOf( - TaskType("1", "FreePrompt", "You can create free prompt text"), - TaskType("2", "Generate Headline", "You can create generate headline text") + override fun getTaskTypes(): List = listOf( + TaskTypeData( + id = "core:text2text", + name = "Free text to text prompt", + description = "Runs an arbitrary prompt through a language model that returns a reply", + inputShape = mapOf( + "input" to Shape( + name = "Prompt", + description = "Describe a task that you want the assistant to do or ask a question", + type = "Text" + ) + ), + outputShape = mapOf( + "output" to Shape( + name = "Generated reply", + description = "The generated text from the assistant", + type = "Text" ) ) - } - } + ) + ) - override fun createTask(input: String, type: String): RemoteOperationResult { - return RemoteOperationResult(RemoteOperationResult.ResultCode.OK) - } + override fun createTask(input: String, taskType: TaskTypeData): RemoteOperationResult = + RemoteOperationResult(RemoteOperationResult.ResultCode.OK) - override fun getTaskList(appId: String): RemoteOperationResult { - val taskList = if (giveEmptyTasks) { - TaskList(listOf()) - } else { - TaskList( - listOf( - Task( - 1, - "FreePrompt", - null, - "12", - "", - "Give me some long text 1", - "Lorem ipsum".getRandomString(100), - "" - ), - Task( - 2, - "GenerateHeadline", - null, - "12", - "", - "Give me some text 2", - "Lorem".getRandomString(100), - "", - "" - ), - Task( - 3, - "FreePrompt", - null, - "12", - "", - "Give me some text 3", - "Lorem".getRandomString(300), - "", - "" - ), - Task( - 4, - "FreePrompt", - null, - "12", - "", - "Give me some text 4", - "Lorem".getRandomString(300), - "", - "" - ), - Task( - 5, - "FreePrompt", - null, - "12", - "", - "Give me some text 5", - "Lorem".getRandomString(300), - "", - "" - ), - Task( - 6, - "FreePrompt", - null, - "12", - "", - "Give me some text 6", - "Lorem".getRandomString(300), - "", - "" - ), - Task( - 7, - "FreePrompt", - null, - "12", - "", - "Give me some text 7", - "Lorem".getRandomString(300), - "", - "" - ) - ) + override fun getTaskList(taskType: String): List = if (giveEmptyTasks) { + listOf() + } else { + listOf( + Task( + 1, + "FreePrompt", + null, + "12", + "", + TaskInput("Give me some long text 1"), + TaskOutput("Lorem ipsum".getRandomString(100)), + 1707692337, + 1707692337, + 1707692337, + 1707692337, + 1707692337 ) - } - - return RemoteOperationResult(RemoteOperationResult.ResultCode.OK).apply { - resultData = taskList - } + ) } - override fun deleteTask(id: Long): RemoteOperationResult { - return RemoteOperationResult(RemoteOperationResult.ResultCode.OK) - } + override fun deleteTask(id: Long): RemoteOperationResult = + RemoteOperationResult(RemoteOperationResult.ResultCode.OK) } diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/AssistantRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/AssistantRepository.kt index 4b3dca4..13830cd 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/AssistantRepository.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/AssistantRepository.kt @@ -1,39 +1,80 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2024 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.assistant.repository import com.nextcloud.common.NextcloudClient import com.owncloud.android.lib.common.operations.RemoteOperationResult -import com.owncloud.android.lib.resources.assistant.CreateTaskRemoteOperation -import com.owncloud.android.lib.resources.assistant.DeleteTaskRemoteOperation -import com.owncloud.android.lib.resources.assistant.GetTaskListRemoteOperation -import com.owncloud.android.lib.resources.assistant.GetTaskTypesRemoteOperation -import com.owncloud.android.lib.resources.assistant.model.TaskList -import com.owncloud.android.lib.resources.assistant.model.TaskTypes +import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode +import com.owncloud.android.lib.resources.assistant.v1.CreateTaskRemoteOperationV1 +import com.owncloud.android.lib.resources.assistant.v1.DeleteTaskRemoteOperationV1 +import com.owncloud.android.lib.resources.assistant.v1.GetTaskListRemoteOperationV1 +import com.owncloud.android.lib.resources.assistant.v1.GetTaskTypesRemoteOperationV1 +import com.owncloud.android.lib.resources.assistant.v1.model.toV2 +import com.owncloud.android.lib.resources.assistant.v2.CreateTaskRemoteOperationV2 +import com.owncloud.android.lib.resources.assistant.v2.DeleteTaskRemoteOperationV2 +import com.owncloud.android.lib.resources.assistant.v2.GetTaskListRemoteOperationV2 +import com.owncloud.android.lib.resources.assistant.v2.GetTaskTypesRemoteOperationV2 +import com.owncloud.android.lib.resources.assistant.v2.model.Task +import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData +import com.owncloud.android.lib.resources.status.NextcloudVersion +import com.owncloud.android.lib.resources.status.OCCapability -class AssistantRepository(private val client: NextcloudClient) : AssistantRepositoryType { +class AssistantRepository(private val client: NextcloudClient, capability: OCCapability) : AssistantRepositoryType { - override fun getTaskTypes(): RemoteOperationResult { - return GetTaskTypesRemoteOperation().execute(client) + private val supportsV2 = capability.version.isNewerOrEqual(NextcloudVersion.nextcloud_30) + + @Suppress("ReturnCount") + override fun getTaskTypes(): List? { + if (supportsV2) { + val result = GetTaskTypesRemoteOperationV2().execute(client) + if (result.isSuccess) { + return result.resultData + } + } else { + val result = GetTaskTypesRemoteOperationV1().execute(client) + if (result.isSuccess) { + return result.resultData.toV2() + } + } + + return null } - override fun createTask( - input: String, - type: String - ): RemoteOperationResult { - return CreateTaskRemoteOperation(input, type).execute(client) + override fun createTask(input: String, taskType: TaskTypeData): RemoteOperationResult = if (supportsV2) { + CreateTaskRemoteOperationV2(input, taskType).execute(client) + } else { + if (taskType.id.isNullOrEmpty()) { + RemoteOperationResult(ResultCode.CANCELLED) + } else { + CreateTaskRemoteOperationV1(input, taskType.id!!).execute(client) + } } - override fun getTaskList(appId: String): RemoteOperationResult { - return GetTaskListRemoteOperation(appId).execute(client) + @Suppress("ReturnCount") + override fun getTaskList(taskType: String): List? { + if (supportsV2) { + val result = GetTaskListRemoteOperationV2(taskType).execute(client) + if (result.isSuccess) { + return result.resultData.tasks.filter { it.appId == "assistant" } + } + } else { + val result = GetTaskListRemoteOperationV1("assistant").execute(client) + if (result.isSuccess) { + return result.resultData.toV2().tasks.filter { it.type == taskType } + } + } + + return null } - override fun deleteTask(id: Long): RemoteOperationResult { - return DeleteTaskRemoteOperation(id).execute(client) + override fun deleteTask(id: Long): RemoteOperationResult = if (supportsV2) { + DeleteTaskRemoteOperationV2(id).execute(client) + } else { + DeleteTaskRemoteOperationV1(id).execute(client) } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/AssistantRepositoryType.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/AssistantRepositoryType.kt index 8aaa9c3..048eee9 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/repository/AssistantRepositoryType.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/AssistantRepositoryType.kt @@ -1,25 +1,21 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2024 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.assistant.repository import com.owncloud.android.lib.common.operations.RemoteOperationResult -import com.owncloud.android.lib.resources.assistant.model.TaskList -import com.owncloud.android.lib.resources.assistant.model.TaskTypes +import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData interface AssistantRepositoryType { - fun getTaskTypes(): RemoteOperationResult + fun getTaskTypes(): List? - fun createTask( - input: String, - type: String - ): RemoteOperationResult + fun createTask(input: String, taskType: TaskTypeData): RemoteOperationResult - fun getTaskList(appId: String): RemoteOperationResult + fun getTaskList(taskType: String): List? fun deleteTask(id: Long): RemoteOperationResult } diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/local/AssistantLocalRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/local/AssistantLocalRepository.kt new file mode 100644 index 0000000..070c0c7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/local/AssistantLocalRepository.kt @@ -0,0 +1,17 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.assistant.repository.local + +import com.owncloud.android.lib.resources.assistant.v2.model.Task + +interface AssistantLocalRepository { + suspend fun cacheTasks(tasks: List, accountName: String) + suspend fun getCachedTasks(accountName: String): List + suspend fun insertTask(task: Task, accountName: String) + suspend fun deleteTask(id: Long, accountName: String) +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/local/AssistantLocalRepositoryImpl.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/local/AssistantLocalRepositoryImpl.kt new file mode 100644 index 0000000..ef6ba93 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/local/AssistantLocalRepositoryImpl.kt @@ -0,0 +1,69 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.assistant.repository.local + +import com.nextcloud.client.database.dao.AssistantDao +import com.nextcloud.client.database.entity.AssistantEntity +import com.owncloud.android.lib.resources.assistant.v2.model.Task +import com.owncloud.android.lib.resources.assistant.v2.model.TaskInput +import com.owncloud.android.lib.resources.assistant.v2.model.TaskOutput + +class AssistantLocalRepositoryImpl(private val assistantDao: AssistantDao) : AssistantLocalRepository { + + override suspend fun cacheTasks(tasks: List, accountName: String) { + val entities = tasks.map { it.toEntity(accountName) } + assistantDao.insertAssistantTasks(entities) + } + + override suspend fun getCachedTasks(accountName: String): List { + val entities = assistantDao.getAssistantTasksByAccount(accountName) + return entities.map { it.toTask() } + } + + override suspend fun insertTask(task: Task, accountName: String) { + assistantDao.insertAssistantTask(task.toEntity(accountName)) + } + + override suspend fun deleteTask(id: Long, accountName: String) { + val cached = assistantDao.getAssistantTasksByAccount(accountName).firstOrNull { it.id == id } ?: return + assistantDao.deleteAssistantTask(cached) + } + + // region Mapping helpers + private fun Task.toEntity(accountName: String): AssistantEntity = AssistantEntity( + id = this.id, + accountName = accountName, + type = this.type, + status = this.status, + userId = this.userId, + appId = this.appId, + input = this.input?.input, + output = this.output?.output, + completionExpectedAt = this.completionExpectedAt, + progress = this.progress, + lastUpdated = this.lastUpdated, + scheduledAt = this.scheduledAt, + endedAt = this.endedAt + ) + + private fun AssistantEntity.toTask(): Task = Task( + id = this.id, + type = this.type, + status = this.status, + userId = this.userId, + appId = this.appId, + input = TaskInput(input = this.input), + output = TaskOutput(output = this.output), + completionExpectedAt = this.completionExpectedAt, + progress = this.progress, + lastUpdated = this.lastUpdated, + scheduledAt = this.scheduledAt, + endedAt = this.endedAt + ) + // endregion +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/local/MockAssistantLocalRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/local/MockAssistantLocalRepository.kt new file mode 100644 index 0000000..c09065a --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/local/MockAssistantLocalRepository.kt @@ -0,0 +1,35 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.assistant.repository.local + +import com.owncloud.android.lib.resources.assistant.v2.model.Task +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +class MockAssistantLocalRepository : AssistantLocalRepository { + + private val tasks = mutableListOf() + private val mutex = Mutex() + + override suspend fun cacheTasks(tasks: List, accountName: String) { + mutex.withLock { + this.tasks.clear() + this.tasks.addAll(tasks) + } + } + + override suspend fun getCachedTasks(accountName: String): List = mutex.withLock { tasks.toList() } + + override suspend fun insertTask(task: Task, accountName: String) { + mutex.withLock { tasks.add(task) } + } + + override suspend fun deleteTask(id: Long, accountName: String) { + mutex.withLock { tasks.removeAll { it.id == id } } + } +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt new file mode 100644 index 0000000..c46245b --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepository.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.assistant.repository.remote + +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.resources.assistant.v2.model.Task +import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData + +interface AssistantRemoteRepository { + fun getTaskTypes(): List? + + fun createTask(input: String, taskType: TaskTypeData): RemoteOperationResult + + fun getTaskList(taskType: String): List? + + fun deleteTask(id: Long): RemoteOperationResult +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt new file mode 100644 index 0000000..07545a3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/AssistantRemoteRepositoryImpl.kt @@ -0,0 +1,80 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.nextcloud.client.assistant.repository.remote + +import com.nextcloud.common.NextcloudClient +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode +import com.owncloud.android.lib.resources.assistant.v1.CreateTaskRemoteOperationV1 +import com.owncloud.android.lib.resources.assistant.v1.DeleteTaskRemoteOperationV1 +import com.owncloud.android.lib.resources.assistant.v1.GetTaskListRemoteOperationV1 +import com.owncloud.android.lib.resources.assistant.v1.GetTaskTypesRemoteOperationV1 +import com.owncloud.android.lib.resources.assistant.v1.model.toV2 +import com.owncloud.android.lib.resources.assistant.v2.CreateTaskRemoteOperationV2 +import com.owncloud.android.lib.resources.assistant.v2.DeleteTaskRemoteOperationV2 +import com.owncloud.android.lib.resources.assistant.v2.GetTaskListRemoteOperationV2 +import com.owncloud.android.lib.resources.assistant.v2.GetTaskTypesRemoteOperationV2 +import com.owncloud.android.lib.resources.assistant.v2.model.Task +import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData +import com.owncloud.android.lib.resources.status.NextcloudVersion +import com.owncloud.android.lib.resources.status.OCCapability + +class AssistantRemoteRepositoryImpl(private val client: NextcloudClient, capability: OCCapability) : + AssistantRemoteRepository { + + private val supportsV2 = capability.version.isNewerOrEqual(NextcloudVersion.nextcloud_30) + + @Suppress("ReturnCount") + override fun getTaskTypes(): List? { + if (supportsV2) { + val result = GetTaskTypesRemoteOperationV2().execute(client) + if (result.isSuccess) { + return result.resultData + } + } else { + val result = GetTaskTypesRemoteOperationV1().execute(client) + if (result.isSuccess) { + return result.resultData.toV2() + } + } + + return null + } + + override fun createTask(input: String, taskType: TaskTypeData): RemoteOperationResult = if (supportsV2) { + CreateTaskRemoteOperationV2(input, taskType).execute(client) + } else { + if (taskType.id.isNullOrEmpty()) { + RemoteOperationResult(ResultCode.CANCELLED) + } else { + CreateTaskRemoteOperationV1(input, taskType.id!!).execute(client) + } + } + + @Suppress("ReturnCount") + override fun getTaskList(taskType: String): List? { + if (supportsV2) { + val result = GetTaskListRemoteOperationV2(taskType).execute(client) + if (result.isSuccess) { + return result.resultData.tasks.filter { it.appId == "assistant" } + } + } else { + val result = GetTaskListRemoteOperationV1("assistant").execute(client) + if (result.isSuccess) { + return result.resultData.toV2().tasks.filter { it.type == taskType } + } + } + + return null + } + + override fun deleteTask(id: Long): RemoteOperationResult = if (supportsV2) { + DeleteTaskRemoteOperationV2(id).execute(client) + } else { + DeleteTaskRemoteOperationV1(id).execute(client) + } +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt new file mode 100644 index 0000000..b7acd88 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/repository/remote/MockAssistantRemoteRepository.kt @@ -0,0 +1,67 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.nextcloud.client.assistant.repository.remote + +import com.nextcloud.utils.extensions.getRandomString +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.resources.assistant.v2.model.Shape +import com.owncloud.android.lib.resources.assistant.v2.model.Task +import com.owncloud.android.lib.resources.assistant.v2.model.TaskInput +import com.owncloud.android.lib.resources.assistant.v2.model.TaskOutput +import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData + +@Suppress("MagicNumber") +class MockAssistantRemoteRepository(private val giveEmptyTasks: Boolean = false) : AssistantRemoteRepository { + override fun getTaskTypes(): List = listOf( + TaskTypeData( + id = "core:text2text", + name = "Free text to text prompt", + description = "Runs an arbitrary prompt through a language model that returns a reply", + inputShape = mapOf( + "input" to Shape( + name = "Prompt", + description = "Describe a task that you want the assistant to do or ask a question", + type = "Text" + ) + ), + outputShape = mapOf( + "output" to Shape( + name = "Generated reply", + description = "The generated text from the assistant", + type = "Text" + ) + ) + ) + ) + + override fun createTask(input: String, taskType: TaskTypeData): RemoteOperationResult = + RemoteOperationResult(RemoteOperationResult.ResultCode.OK) + + override fun getTaskList(taskType: String): List = if (giveEmptyTasks) { + listOf() + } else { + listOf( + Task( + 1, + "FreePrompt", + null, + "12", + "", + TaskInput("Give me some long text 1"), + TaskOutput("Lorem ipsum".getRandomString(100)), + 1707692337, + 1707692337, + 1707692337, + 1707692337, + 1707692337 + ) + ) + } + + override fun deleteTask(id: Long): RemoteOperationResult = + RemoteOperationResult(RemoteOperationResult.ResultCode.OK) +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/task/TaskStatus.kt b/app/src/main/java/com/nextcloud/client/assistant/task/TaskStatus.kt deleted file mode 100644 index c917988..0000000 --- a/app/src/main/java/com/nextcloud/client/assistant/task/TaskStatus.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2024 Your Name - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.nextcloud.client.assistant.task - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.nextcloud.client.assistant.extensions.statusData -import com.owncloud.android.lib.resources.assistant.model.Task - -@Composable -fun TaskStatus(task: Task, foregroundColor: Color) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - val (iconId, descriptionId) = task.statusData() - - Image( - painter = painterResource(id = iconId), - modifier = Modifier.size(16.dp), - colorFilter = ColorFilter.tint(foregroundColor), - contentDescription = "status icon" - ) - - Spacer(modifier = Modifier.width(6.dp)) - - Text(text = stringResource(id = descriptionId), color = foregroundColor) - - /* - Spacer(modifier = Modifier.weight(1f)) - - Text(text = task.completionDateRepresentation(), color = foregroundColor) - - Spacer(modifier = Modifier.width(6.dp)) - */ - } -} diff --git a/app/src/main/java/com/nextcloud/client/assistant/task/TaskStatusView.kt b/app/src/main/java/com/nextcloud/client/assistant/task/TaskStatusView.kt new file mode 100644 index 0000000..2f2f8d7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/assistant/task/TaskStatusView.kt @@ -0,0 +1,151 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ + +package com.nextcloud.client.assistant.task + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.nextcloud.client.assistant.extensions.getModifiedAtRepresentation +import com.nextcloud.client.assistant.extensions.getStatusIcon +import com.owncloud.android.R +import com.owncloud.android.lib.resources.assistant.v2.model.Task +import com.owncloud.android.lib.resources.assistant.v2.model.TaskInput +import com.owncloud.android.lib.resources.assistant.v2.model.TaskOutput +import com.owncloud.android.lib.resources.status.OCCapability +import java.util.concurrent.TimeUnit + +@Composable +fun TaskStatusView(task: Task, capability: OCCapability) { + val context = LocalContext.current + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val iconId = task.getStatusIcon(capability) + val description = task.getModifiedAtRepresentation(context) + + Image( + painter = painterResource(id = iconId), + modifier = Modifier.size(16.dp), + colorFilter = ColorFilter.tint(color = colorResource(R.color.text_color)), + contentDescription = "status icon" + ) + + description?.let { + Spacer(modifier = Modifier.width(6.dp)) + Text(text = description, color = colorResource(R.color.text_color)) + } + } +} + +@Suppress("LongMethod", "MagicNumber") +@Composable +@Preview +private fun TaskStatusViewPreview() { + val currentTime = System.currentTimeMillis() / 1000 + + val tasks = listOf( + Task( + id = 1L, + type = "type1", + status = "STATUS_RUNNING", + userId = "user1", + appId = "app1", + input = TaskInput("input1"), + output = TaskOutput("output1"), + scheduledAt = currentTime.toInt(), + lastUpdated = currentTime.toInt() + ), + + Task( + id = 2L, + type = "type2", + status = "STATUS_SUCCESSFUL", + userId = "user2", + appId = "app2", + input = TaskInput("input2"), + output = TaskOutput("output2"), + lastUpdated = (currentTime - TimeUnit.MINUTES.toSeconds(5)).toInt() + ), + + Task( + id = 3L, + type = "type3", + status = "STATUS_RUNNING", + userId = "user3", + appId = "app3", + input = TaskInput("input3"), + output = TaskOutput("output3"), + lastUpdated = (currentTime - TimeUnit.HOURS.toSeconds(5)).toInt() + ), + + Task( + id = 4L, + type = "type4", + status = "STATUS_SUCCESSFUL", + userId = "user4", + appId = "app4", + input = TaskInput("input4"), + output = TaskOutput("output4"), + lastUpdated = (currentTime - TimeUnit.DAYS.toSeconds(5)).toInt() + ), + + Task( + id = 5L, + type = "type5", + status = "STATUS_SUCCESSFUL", + userId = "user5", + appId = "app5", + input = TaskInput("input5"), + output = TaskOutput("output5"), + lastUpdated = (currentTime - TimeUnit.DAYS.toSeconds(60)).toInt() + ), + + Task( + id = 6L, + type = "type7", + status = "STATUS_UNKNOWN", + userId = "user7", + appId = "app7", + input = TaskInput("input7"), + output = TaskOutput("output7"), + scheduledAt = null, + lastUpdated = null + ) + ) + + LazyColumn { + items(tasks) { + TaskStatusView( + it, + OCCapability().apply { + versionMayor = 30 + } + ) + } + } +} diff --git a/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt b/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt index 865f41b..d0792f6 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/task/TaskView.kt @@ -1,118 +1,120 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Your Name - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.assistant.task import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.MaterialTheme +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.nextcloud.client.assistant.taskDetail.TaskDetailBottomSheet -import com.nextcloud.ui.composeComponents.bottomSheet.MoreActionsBottomSheet -import com.nextcloud.utils.extensions.getRandomString +import com.nextcloud.utils.extensions.truncateWithEllipsis import com.owncloud.android.R -import com.owncloud.android.lib.resources.assistant.model.Task +import com.owncloud.android.lib.resources.assistant.v2.model.Task +import com.owncloud.android.lib.resources.assistant.v2.model.TaskInput +import com.owncloud.android.lib.resources.assistant.v2.model.TaskOutput +import com.owncloud.android.lib.resources.status.OCCapability -@OptIn(ExperimentalFoundationApi::class) @Suppress("LongMethod", "MagicNumber") @Composable -fun TaskView( - task: Task, - showDeleteTaskAlertDialog: (Long) -> Unit -) { +fun TaskView(task: Task, capability: OCCapability, showTaskActions: () -> Unit) { var showTaskDetailBottomSheet by remember { mutableStateOf(false) } - var showMoreActionsBottomSheet by remember { mutableStateOf(false) } - Column( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(16.dp)) - .background(MaterialTheme.colorScheme.primary) - .combinedClickable(onClick = { - showTaskDetailBottomSheet = true - }, onLongClick = { - showMoreActionsBottomSheet = true - }) - .padding(start = 8.dp) - ) { - Spacer(modifier = Modifier.height(8.dp)) - - task.input?.let { - Text( - text = it, - color = Color.White, - fontSize = 18.sp - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - - task.output?.let { - HorizontalDivider(modifier = Modifier.padding(horizontal = 4.dp, vertical = 8.dp)) - - Text( - text = it.take(100), - fontSize = 12.sp, - color = Color.White, - modifier = Modifier - .height(100.dp) - .animateContentSize( - animationSpec = spring( - dampingRatio = Spring.DampingRatioLowBouncy, - stiffness = Spring.StiffnessLow - ) - ) - ) - } - - TaskStatus(task, foregroundColor = Color.White) - - if (showMoreActionsBottomSheet) { - val bottomSheetAction = listOf( - Triple( - R.drawable.ic_delete, - R.string.assistant_screen_task_more_actions_bottom_sheet_delete_action - ) { - showDeleteTaskAlertDialog(task.id) + Box { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(color = colorResource(R.color.task_container)) + .clickable { + showTaskDetailBottomSheet = true } - ) + .padding(16.dp) + ) { + Spacer(modifier = Modifier.height(8.dp)) - MoreActionsBottomSheet( - title = task.input, - actions = bottomSheetAction, - dismiss = { showMoreActionsBottomSheet = false } - ) + task.input?.input?.let { + Text( + text = it.truncateWithEllipsis(30), + color = colorResource(R.color.text_color), + fontSize = 18.sp, + textAlign = TextAlign.Left, + maxLines = 1, + fontWeight = FontWeight.Bold, + modifier = Modifier.width(300.dp) + ) + } + + Spacer(modifier = Modifier.height(12.dp)) + + task.output?.output?.let { + Text( + text = it.truncateWithEllipsis(100), + fontSize = 18.sp, + color = colorResource(R.color.text_color), + textAlign = TextAlign.Left, + modifier = Modifier + .animateContentSize( + animationSpec = spring( + dampingRatio = Spring.DampingRatioLowBouncy, + stiffness = Spring.StiffnessLow + ) + ) + ) + } + + TaskStatusView(task, capability) + + if (showTaskDetailBottomSheet) { + TaskDetailBottomSheet(task, showTaskActions = { + showTaskDetailBottomSheet = false + showTaskActions() + }) { + showTaskDetailBottomSheet = false + } + } } - if (showTaskDetailBottomSheet) { - TaskDetailBottomSheet(task) { - showTaskDetailBottomSheet = false - } + IconButton( + modifier = Modifier.align(Alignment.TopEnd), + onClick = showTaskActions + ) { + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = "More button", + tint = colorResource(R.color.text_color) + ) } } } @@ -121,20 +123,28 @@ fun TaskView( @Preview @Composable private fun TaskViewPreview() { - val output = "Lorem".getRandomString(100) - TaskView( task = Task( 1, "Free Prompt", - 0, + "STATUS_COMPLETED", "1", "1", - "Give me text", - output, - "", - "" - ) - ) { - } + TaskInput("What about other promising tokens like"), + TaskOutput( + "Several tokens show promise for future growth in the" + + "cryptocurrency market" + ), + 1707692337, + 1707692337, + 1707692337, + 1707692337, + 1707692337 + ), + OCCapability().apply { + versionMayor = 30 + }, + showTaskActions = { + } + ) } diff --git a/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt b/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt index e7148de..a52b114 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/taskDetail/TaskDetailBottomSheet.kt @@ -1,17 +1,17 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Your Name - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.assistant.taskDetail -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.spring import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -19,128 +19,141 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.nextcloud.client.assistant.task.TaskStatus import com.nextcloud.utils.extensions.getRandomString import com.owncloud.android.R -import com.owncloud.android.lib.resources.assistant.model.Task +import com.owncloud.android.lib.resources.assistant.v2.model.Task +import com.owncloud.android.lib.resources.assistant.v2.model.TaskInput +import com.owncloud.android.lib.resources.assistant.v2.model.TaskOutput @Suppress("LongMethod") @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable -fun TaskDetailBottomSheet(task: Task, dismiss: () -> Unit) { - var showInput by remember { mutableStateOf(true) } +fun TaskDetailBottomSheet(task: Task, showTaskActions: () -> Unit, dismiss: () -> Unit) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) ModalBottomSheet( modifier = Modifier.padding(top = 32.dp), - containerColor = Color.White, - onDismissRequest = { - dismiss() - }, + containerColor = colorResource(R.color.bg_default), + onDismissRequest = { dismiss() }, sheetState = sheetState ) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(16.dp) - ) { - stickyHeader { - Row( - modifier = Modifier - .fillMaxWidth() - .background(color = colorResource(id = R.color.light_grey), shape = RoundedCornerShape(8.dp)) - ) { - TextInputSelectButton( - Modifier.weight(1f), - R.string.assistant_task_detail_screen_input_button_title, - showInput, - onClick = { - showInput = true - } - ) + Box { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + stickyHeader { + Row( + modifier = Modifier.fillMaxWidth() + ) { + Spacer(modifier = Modifier.weight(1f)) - TextInputSelectButton( - Modifier.weight(1f), - R.string.assistant_task_detail_screen_output_button_title, - !showInput, - onClick = { - showInput = false + IconButton(onClick = showTaskActions) { + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = "More button", + tint = colorResource(R.color.text_color) + ) } - ) + } + } + + item { + InputOutputCard(task) } } - item { - Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(bottom = 16.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(R.drawable.ic_assistant), + contentDescription = "assistant icon", + modifier = Modifier.size(12.dp) + ) - Column( - modifier = Modifier - .fillMaxSize() - .background(color = colorResource(id = R.color.light_grey), shape = RoundedCornerShape(8.dp)) - .padding(16.dp) - ) { - Text( - text = if (showInput) { - task.input ?: "" - } else { - task.output ?: "" - }, - fontSize = 12.sp, - color = Color.Black, - modifier = Modifier - .animateContentSize( - animationSpec = spring( - dampingRatio = Spring.DampingRatioLowBouncy, - stiffness = Spring.StiffnessLow - ) - ) - ) - } + Spacer(modifier = Modifier.width(4.dp)) - TaskStatus(task, foregroundColor = Color.Black) - - Spacer(modifier = Modifier.height(32.dp)) + Text( + text = stringResource(R.string.assistant_generation_warning), + color = colorResource(R.color.text_color), + fontSize = 12.sp + ) } } } } @Composable -private fun TextInputSelectButton(modifier: Modifier, titleId: Int, highlightCondition: Boolean, onClick: () -> Unit) { - Button( - onClick = onClick, - shape = RoundedCornerShape(8.dp), - colors = if (highlightCondition) { - ButtonDefaults.buttonColors(containerColor = Color.White) - } else { - ButtonDefaults.buttonColors(containerColor = colorResource(id = R.color.light_grey)) - }, - modifier = modifier - .widthIn(min = 0.dp, max = 200.dp) - .padding(horizontal = 4.dp) +fun InputOutputCard(task: Task) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(Color.Transparent, shape = RoundedCornerShape(8.dp)) ) { - Text(text = stringResource(id = titleId), color = Color.Black) + TitleDescriptionBox( + title = stringResource(R.string.assistant_task_detail_screen_input_button_title), + description = task.input?.input ?: "" + ) + + Spacer(modifier = Modifier.height(16.dp)) + + TitleDescriptionBox( + title = stringResource(R.string.assistant_task_detail_screen_output_button_title), + description = task.output?.output ?: stringResource(R.string.assistant_screen_task_output_empty_text) + ) + } +} + +@Composable +private fun TitleDescriptionBox(title: String, description: String?) { + Text( + text = title, + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + color = colorResource(R.color.text_color) + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .background(color = colorResource(R.color.task_container), RoundedCornerShape(8.dp)) + .padding(12.dp) + ) { + Text( + text = description ?: "", + color = colorResource(R.color.text_color) + ) } } @@ -152,14 +165,19 @@ private fun TaskDetailScreenPreview() { task = Task( 1, "Free Prompt", - 0, + null, "1", "1", - "Give me text".getRandomString(100), - "output".getRandomString(300), - "", - "" - ) + TaskInput("Give me text".getRandomString(100)), + TaskOutput("output".getRandomString(300)), + 1707692337, + 1707692337, + 1707692337, + 1707692337, + 1707692337 + ), + showTaskActions = { + } ) { } } diff --git a/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt b/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt index eab2687..3e0e9ad 100644 --- a/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt +++ b/app/src/main/java/com/nextcloud/client/assistant/taskTypes/TaskTypesRow.kt @@ -1,51 +1,65 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2024 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.assistant.taskTypes -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.FilledTonalButton +import android.annotation.SuppressLint +import androidx.compose.material3.PrimaryScrollableTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.owncloud.android.lib.resources.assistant.model.TaskType +import com.owncloud.android.R +import com.owncloud.android.lib.resources.assistant.v2.model.TaskTypeData +@SuppressLint("ResourceType") @Composable -fun TaskTypesRow(selectedTaskType: TaskType?, data: List?, selectTaskType: (TaskType) -> Unit) { - Row( - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()) - ) { - data?.forEach { taskType -> - taskType.name?.let { taskTypeName -> - FilledTonalButton( - onClick = { selectTaskType(taskType) }, - colors = ButtonDefaults.buttonColors( - containerColor = if (selectedTaskType?.id == taskType.id) { - Color.Unspecified - } else { - Color.Gray - } - ) - ) { - Text(text = taskTypeName) - } +fun TaskTypesRow(selectedTaskType: TaskTypeData?, data: List, selectTaskType: (TaskTypeData) -> Unit) { + val selectedTabIndex = data.indexOfFirst { it.id == selectedTaskType?.id }.takeIf { it >= 0 } ?: 0 - Spacer(modifier = Modifier.padding(end = 8.dp)) + PrimaryScrollableTabRow( + selectedTabIndex = selectedTabIndex, + edgePadding = 0.dp, + containerColor = colorResource(R.color.actionbar_color), + indicator = { + TabRowDefaults.SecondaryIndicator( + Modifier.tabIndicatorOffset(selectedTabIndex), + color = colorResource(R.color.primary) + ) + } + ) { + data.forEach { taskType -> + if (taskType.name.isNotEmpty()) { + Tab( + selected = selectedTaskType?.id == taskType.id, + onClick = { selectTaskType(taskType) }, + selectedContentColor = colorResource(R.color.text_color), + unselectedContentColor = colorResource(R.color.disabled_text), + text = { Text(text = taskType.name) } + ) } } } } + +@Composable +@Preview +private fun TaskTypesRowPreview() { + val selectedTaskType = TaskTypeData("1", "Free text to text prompt", "", emptyMap(), emptyMap()) + val taskTypes = listOf( + TaskTypeData("1", "Free text to text prompt", "", emptyMap(), emptyMap()), + TaskTypeData("2", "Extract topics", "", emptyMap(), emptyMap()), + TaskTypeData("3", "Generate Headline", "", emptyMap(), emptyMap()), + TaskTypeData("4", "Summarize", "", emptyMap(), emptyMap()) + ) + + TaskTypesRow(selectedTaskType, taskTypes) { } +} diff --git a/app/src/main/java/com/nextcloud/client/core/AsyncRunner.kt b/app/src/main/java/com/nextcloud/client/core/AsyncRunner.kt index 55b377f..156299c 100644 --- a/app/src/main/java/com/nextcloud/client/core/AsyncRunner.kt +++ b/app/src/main/java/com/nextcloud/client/core/AsyncRunner.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.core diff --git a/app/src/main/java/com/nextcloud/client/core/Cancellable.kt b/app/src/main/java/com/nextcloud/client/core/Cancellable.kt index 07f6ece..330d0fe 100644 --- a/app/src/main/java/com/nextcloud/client/core/Cancellable.kt +++ b/app/src/main/java/com/nextcloud/client/core/Cancellable.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.core diff --git a/app/src/main/java/com/nextcloud/client/core/Clock.kt b/app/src/main/java/com/nextcloud/client/core/Clock.kt index 744aae9..07d8c50 100644 --- a/app/src/main/java/com/nextcloud/client/core/Clock.kt +++ b/app/src/main/java/com/nextcloud/client/core/Clock.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.core diff --git a/app/src/main/java/com/nextcloud/client/core/ClockImpl.kt b/app/src/main/java/com/nextcloud/client/core/ClockImpl.kt index e0c170e..71c252e 100644 --- a/app/src/main/java/com/nextcloud/client/core/ClockImpl.kt +++ b/app/src/main/java/com/nextcloud/client/core/ClockImpl.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.core diff --git a/app/src/main/java/com/nextcloud/client/core/LocalBinder.kt b/app/src/main/java/com/nextcloud/client/core/LocalBinder.kt index 8d28140..d3b6a09 100644 --- a/app/src/main/java/com/nextcloud/client/core/LocalBinder.kt +++ b/app/src/main/java/com/nextcloud/client/core/LocalBinder.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.core diff --git a/app/src/main/java/com/nextcloud/client/core/LocalConnection.kt b/app/src/main/java/com/nextcloud/client/core/LocalConnection.kt index 9ca3f81..5e67d24 100644 --- a/app/src/main/java/com/nextcloud/client/core/LocalConnection.kt +++ b/app/src/main/java/com/nextcloud/client/core/LocalConnection.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.core @@ -19,9 +19,7 @@ import android.os.IBinder * * One can subclass it to create own service interaction API. */ -abstract class LocalConnection( - protected val context: Context -) : ServiceConnection { +abstract class LocalConnection(protected val context: Context) : ServiceConnection { private var serviceBinder: LocalBinder? = null val service: S? get() = serviceBinder?.service @@ -35,9 +33,7 @@ abstract class LocalConnection( * * @see [bind] */ - protected open fun createBindIntent(): Intent? { - return null - } + protected open fun createBindIntent(): Intent? = null /** * Bind local service. If [createBindIntent] returns null, it no-ops. diff --git a/app/src/main/java/com/nextcloud/client/core/ManualAsyncRunner.kt b/app/src/main/java/com/nextcloud/client/core/ManualAsyncRunner.kt index 60a5fd3..d301eb1 100644 --- a/app/src/main/java/com/nextcloud/client/core/ManualAsyncRunner.kt +++ b/app/src/main/java/com/nextcloud/client/core/ManualAsyncRunner.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.core @@ -20,14 +20,12 @@ class ManualAsyncRunner : AsyncRunner { task: () -> T, onResult: OnResultCallback?, onError: OnErrorCallback? - ): Cancellable { - return postTask( - task = { _: OnProgressCallback, _: IsCancelled -> task.invoke() }, - onResult = onResult, - onError = onError, - onProgress = null - ) - } + ): Cancellable = postTask( + task = { _: OnProgressCallback, _: IsCancelled -> task.invoke() }, + onResult = onResult, + onError = onError, + onProgress = null + ) override fun postTask( task: TaskFunction, diff --git a/app/src/main/java/com/nextcloud/client/core/Task.kt b/app/src/main/java/com/nextcloud/client/core/Task.kt index fc2e989..6c04e3a 100644 --- a/app/src/main/java/com/nextcloud/client/core/Task.kt +++ b/app/src/main/java/com/nextcloud/client/core/Task.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.core @@ -20,7 +20,8 @@ internal class Task( private val onSuccess: OnResultCallback?, private val onError: OnErrorCallback?, private val onProgress: OnProgressCallback

? -) : Runnable, Cancellable { +) : Runnable, + Cancellable { val isCancelled: Boolean get() = cancelled.get() diff --git a/app/src/main/java/com/nextcloud/client/core/ThreadPoolAsyncRunner.kt b/app/src/main/java/com/nextcloud/client/core/ThreadPoolAsyncRunner.kt index 1fbed82..0c6da24 100644 --- a/app/src/main/java/com/nextcloud/client/core/ThreadPoolAsyncRunner.kt +++ b/app/src/main/java/com/nextcloud/client/core/ThreadPoolAsyncRunner.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.core diff --git a/app/src/main/java/com/nextcloud/client/database/DatabaseModule.kt b/app/src/main/java/com/nextcloud/client/database/DatabaseModule.kt index 2e2c4e9..a0989a2 100644 --- a/app/src/main/java/com/nextcloud/client/database/DatabaseModule.kt +++ b/app/src/main/java/com/nextcloud/client/database/DatabaseModule.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.database @@ -11,6 +11,7 @@ import android.content.Context import com.nextcloud.client.core.Clock import com.nextcloud.client.database.dao.ArbitraryDataDao import com.nextcloud.client.database.dao.FileDao +import com.nextcloud.client.database.dao.OfflineOperationDao import dagger.Module import dagger.Provides import javax.inject.Singleton @@ -20,17 +21,15 @@ class DatabaseModule { @Provides @Singleton - fun database(context: Context, clock: Clock): NextcloudDatabase { - return NextcloudDatabase.getInstance(context, clock) - } + fun database(context: Context, clock: Clock): NextcloudDatabase = NextcloudDatabase.getInstance(context, clock) @Provides - fun arbitraryDataDao(nextcloudDatabase: NextcloudDatabase): ArbitraryDataDao { - return nextcloudDatabase.arbitraryDataDao() - } + fun arbitraryDataDao(nextcloudDatabase: NextcloudDatabase): ArbitraryDataDao = nextcloudDatabase.arbitraryDataDao() @Provides - fun fileDao(nextcloudDatabase: NextcloudDatabase): FileDao { - return nextcloudDatabase.fileDao() - } + fun fileDao(nextcloudDatabase: NextcloudDatabase): FileDao = nextcloudDatabase.fileDao() + + @Provides + fun offlineOperationsDao(nextcloudDatabase: NextcloudDatabase): OfflineOperationDao = + nextcloudDatabase.offlineOperationDao() } diff --git a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt index db98e77..87ce9c9 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.database @@ -12,23 +12,36 @@ import androidx.room.AutoMigration import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import androidx.room.TypeConverters import com.nextcloud.client.core.Clock import com.nextcloud.client.core.ClockImpl import com.nextcloud.client.database.dao.ArbitraryDataDao +import com.nextcloud.client.database.dao.AssistantDao import com.nextcloud.client.database.dao.FileDao +import com.nextcloud.client.database.dao.FileSystemDao +import com.nextcloud.client.database.dao.OfflineOperationDao +import com.nextcloud.client.database.dao.RecommendedFileDao +import com.nextcloud.client.database.dao.SyncedFolderDao +import com.nextcloud.client.database.dao.UploadDao import com.nextcloud.client.database.entity.ArbitraryDataEntity +import com.nextcloud.client.database.entity.AssistantEntity import com.nextcloud.client.database.entity.CapabilityEntity import com.nextcloud.client.database.entity.ExternalLinkEntity import com.nextcloud.client.database.entity.FileEntity import com.nextcloud.client.database.entity.FilesystemEntity +import com.nextcloud.client.database.entity.OfflineOperationEntity +import com.nextcloud.client.database.entity.RecommendedFileEntity import com.nextcloud.client.database.entity.ShareEntity import com.nextcloud.client.database.entity.SyncedFolderEntity import com.nextcloud.client.database.entity.UploadEntity import com.nextcloud.client.database.entity.VirtualEntity import com.nextcloud.client.database.migrations.DatabaseMigrationUtil +import com.nextcloud.client.database.migrations.MIGRATION_88_89 import com.nextcloud.client.database.migrations.Migration67to68 import com.nextcloud.client.database.migrations.RoomMigration import com.nextcloud.client.database.migrations.addLegacyMigrations +import com.nextcloud.client.database.typeConverter.OfflineOperationTypeConverter +import com.owncloud.android.MainApp import com.owncloud.android.db.ProviderMeta @Database( @@ -41,7 +54,10 @@ import com.owncloud.android.db.ProviderMeta ShareEntity::class, SyncedFolderEntity::class, UploadEntity::class, - VirtualEntity::class + VirtualEntity::class, + OfflineOperationEntity::class, + RecommendedFileEntity::class, + AssistantEntity::class ], version = ProviderMeta.DB_VERSION, autoMigrations = [ @@ -59,40 +75,65 @@ import com.owncloud.android.db.ProviderMeta AutoMigration(from = 77, to = 78), AutoMigration(from = 78, to = 79, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), AutoMigration(from = 79, to = 80), - AutoMigration(from = 80, to = 81) + AutoMigration(from = 80, to = 81), + AutoMigration(from = 81, to = 82), + AutoMigration(from = 82, to = 83), + AutoMigration(from = 83, to = 84), + AutoMigration(from = 84, to = 85, spec = DatabaseMigrationUtil.DeleteColumnSpec::class), + AutoMigration(from = 85, to = 86, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), + AutoMigration(from = 86, to = 87, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), + AutoMigration(from = 87, to = 88, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), + // manual migration used for 88 to 89 + AutoMigration(from = 89, to = 90), + AutoMigration(from = 90, to = 91), + AutoMigration(from = 91, to = 92), + AutoMigration(from = 92, to = 93, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), + AutoMigration(from = 93, to = 94, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), + AutoMigration(from = 94, to = 95, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), + AutoMigration(from = 95, to = 96) ], exportSchema = true ) @Suppress("Detekt.UnnecessaryAbstractClass") // needed by Room +@TypeConverters(OfflineOperationTypeConverter::class) abstract class NextcloudDatabase : RoomDatabase() { abstract fun arbitraryDataDao(): ArbitraryDataDao abstract fun fileDao(): FileDao + abstract fun offlineOperationDao(): OfflineOperationDao + abstract fun uploadDao(): UploadDao + abstract fun recommendedFileDao(): RecommendedFileDao + abstract fun fileSystemDao(): FileSystemDao + abstract fun syncedFolderDao(): SyncedFolderDao + abstract fun assistantDao(): AssistantDao companion object { const val FIRST_ROOM_DB_VERSION = 65 - private var INSTANCE: NextcloudDatabase? = null + private var instance: NextcloudDatabase? = null @JvmStatic @Suppress("DeprecatedCallableAddReplaceWith") @Deprecated("Here for legacy purposes, inject this class or use getInstance(context, clock) instead") - fun getInstance(context: Context): NextcloudDatabase { - return getInstance(context, ClockImpl()) - } + fun getInstance(context: Context): NextcloudDatabase = getInstance(context, ClockImpl()) @JvmStatic fun getInstance(context: Context, clock: Clock): NextcloudDatabase { - if (INSTANCE == null) { - INSTANCE = Room + if (instance == null) { + instance = Room .databaseBuilder(context, NextcloudDatabase::class.java, ProviderMeta.DB_NAME) .allowMainThreadQueries() + .addTypeConverter(OfflineOperationTypeConverter()) .addLegacyMigrations(clock, context) .addMigrations(RoomMigration()) .addMigrations(Migration67to68()) - .fallbackToDestructiveMigration() + .addMigrations(MIGRATION_88_89) .build() } - return INSTANCE!! + return instance!! } + + @Suppress("DEPRECATION") + @JvmStatic + fun instance(): NextcloudDatabase = getInstance(MainApp.getAppContext()) } } diff --git a/app/src/main/java/com/nextcloud/client/database/dao/ArbitraryDataDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/ArbitraryDataDao.kt index 57afe3e..507e886 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/ArbitraryDataDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/ArbitraryDataDao.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.database.dao diff --git a/app/src/main/java/com/nextcloud/client/database/dao/AssistantDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/AssistantDao.kt new file mode 100644 index 0000000..9ba9012 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/dao/AssistantDao.kt @@ -0,0 +1,41 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.nextcloud.client.database.entity.AssistantEntity +import com.owncloud.android.db.ProviderMeta + +@Dao +interface AssistantDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAssistantTask(task: AssistantEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAssistantTasks(tasks: List) + + @Update + suspend fun updateAssistantTask(task: AssistantEntity) + + @Delete + suspend fun deleteAssistantTask(task: AssistantEntity) + + @Query( + """ + SELECT * FROM ${ProviderMeta.ProviderTableMeta.ASSISTANT_TABLE_NAME} + WHERE accountName = :accountName + ORDER BY lastUpdated DESC +""" + ) + suspend fun getAssistantTasksByAccount(accountName: String): List +} diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt index e04cc27..3356d69 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt @@ -3,20 +3,29 @@ * * SPDX-FileCopyrightText: 2022 Dariusz Olszewski * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.database.dao import androidx.room.Dao import androidx.room.Query +import androidx.room.Update import com.nextcloud.client.database.entity.FileEntity import com.owncloud.android.db.ProviderMeta.ProviderTableMeta +import com.owncloud.android.utils.MimeType +@Suppress("TooManyFunctions") @Dao interface FileDao { + @Update + fun update(entity: FileEntity) + @Query("SELECT * FROM filelist WHERE _id = :id LIMIT 1") fun getFileById(id: Long): FileEntity? + @Query("SELECT * FROM filelist WHERE local_id = :localId LIMIT 1") + fun getFileByLocalId(localId: Long): FileEntity? + @Query("SELECT * FROM filelist WHERE path = :path AND file_owner = :fileOwner LIMIT 1") fun getFileByEncryptedRemotePath(path: String, fileOwner: String): FileEntity? @@ -49,4 +58,57 @@ interface FileDao { @Query("SELECT * FROM filelist where file_owner = :fileOwner AND etag_in_conflict IS NOT NULL") fun getFilesWithSyncConflict(fileOwner: String): List + + @Query( + "SELECT * FROM filelist where file_owner = :fileOwner AND internal_two_way_sync_timestamp >= 0 " + + "ORDER BY internal_two_way_sync_timestamp DESC" + ) + fun getInternalTwoWaySyncFolders(fileOwner: String): List + + @Query( + """ + SELECT * + FROM filelist + WHERE parent = :parentId + AND file_owner = :accountName + AND is_encrypted = 0 + AND (content_type = :dirType OR content_type = :webdavType) + ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER} + """ + ) + fun getNonEncryptedSubfolders( + parentId: Long, + accountName: String, + dirType: String = MimeType.DIRECTORY, + webdavType: String = MimeType.WEBDAV_FOLDER + ): List + + @Query( + """ + SELECT * + FROM filelist + WHERE parent = :parentId + AND file_owner = :accountName + AND (content_type != :dirType AND content_type != :webdavType) + ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER} + """ + ) + fun getSubfiles( + parentId: Long, + accountName: String, + dirType: String = MimeType.DIRECTORY, + webdavType: String = MimeType.WEBDAV_FOLDER + ): List + + @Query( + """ + SELECT * + FROM filelist + WHERE file_owner = :fileOwner + AND parent = :parentId + AND ${ProviderTableMeta.FILE_NAME} LIKE '%' || :query || '%' + ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER} + """ + ) + fun searchFilesInFolder(parentId: Long, fileOwner: String, query: String): List } diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileSystemDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileSystemDao.kt new file mode 100644 index 0000000..fcb567a --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileSystemDao.kt @@ -0,0 +1,40 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.dao + +import androidx.room.Dao +import androidx.room.Query +import com.nextcloud.client.database.entity.FilesystemEntity +import com.owncloud.android.db.ProviderMeta + +@Dao +interface FileSystemDao { + @Query( + """ + SELECT * + FROM ${ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME} + WHERE ${ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID} = :syncedFolderId + AND ${ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD} = 0 + AND ${ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER} = 0 + AND ${ProviderMeta.ProviderTableMeta._ID} > :lastId + ORDER BY ${ProviderMeta.ProviderTableMeta._ID} + LIMIT :limit + """ + ) + suspend fun getAutoUploadFilesEntities(syncedFolderId: String, limit: Int, lastId: Int): List + + @Query( + """ + UPDATE ${ProviderMeta.ProviderTableMeta.FILESYSTEM_TABLE_NAME} + SET ${ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD} = 1 + WHERE ${ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH} = :localPath + AND ${ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID} = :syncedFolderId + """ + ) + suspend fun markFileAsUploaded(localPath: String, syncedFolderId: String) +} diff --git a/app/src/main/java/com/nextcloud/client/database/dao/OfflineOperationDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/OfflineOperationDao.kt new file mode 100644 index 0000000..50817da --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/dao/OfflineOperationDao.kt @@ -0,0 +1,46 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.nextcloud.client.database.entity.OfflineOperationEntity + +@Dao +interface OfflineOperationDao { + @Query("SELECT * FROM offline_operations") + fun getAll(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg entity: OfflineOperationEntity) + + @Update + fun update(entity: OfflineOperationEntity) + + @Delete + fun delete(entity: OfflineOperationEntity) + + @Query("DELETE FROM offline_operations WHERE offline_operations_path = :path") + fun deleteByPath(path: String) + + @Query("SELECT * FROM offline_operations WHERE offline_operations_path = :path LIMIT 1") + fun getByPath(path: String): OfflineOperationEntity? + + @Query("SELECT * FROM offline_operations WHERE offline_operations_parent_oc_file_id = :parentOCFileId") + fun getSubEntitiesByParentOCFileId(parentOCFileId: Long): List + + @Query("DELETE FROM offline_operations") + fun clearTable() + + @Query("DELETE FROM offline_operations WHERE _id = :id") + fun deleteById(id: Int) +} diff --git a/app/src/main/java/com/nextcloud/client/database/dao/RecommendedFileDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/RecommendedFileDao.kt new file mode 100644 index 0000000..a93f857 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/dao/RecommendedFileDao.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.nextcloud.client.database.entity.RecommendedFileEntity +import com.owncloud.android.db.ProviderMeta + +@Dao +interface RecommendedFileDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(recommendedFiles: List) + + @Query( + "SELECT * FROM ${ProviderMeta.ProviderTableMeta.RECOMMENDED_FILE_TABLE_NAME} WHERE account_name = :accountName" + ) + suspend fun getAll(accountName: String): List +} diff --git a/app/src/main/java/com/nextcloud/client/database/dao/SyncedFolderDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/SyncedFolderDao.kt new file mode 100644 index 0000000..ddb35ff --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/dao/SyncedFolderDao.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.dao + +import androidx.room.Dao +import androidx.room.Query +import com.nextcloud.client.database.entity.SyncedFolderEntity +import com.owncloud.android.db.ProviderMeta + +@Dao +interface SyncedFolderDao { + @Query( + """ + SELECT * FROM ${ProviderMeta.ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME} + WHERE ${ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH} = :localPath + AND ${ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ACCOUNT} = :account + LIMIT 1 + """ + ) + fun findByLocalPathAndAccount(localPath: String, account: String): SyncedFolderEntity? +} diff --git a/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt new file mode 100644 index 0000000..deaa44c --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt @@ -0,0 +1,96 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.nextcloud.client.database.entity.UploadEntity +import com.owncloud.android.db.ProviderMeta.ProviderTableMeta + +@Dao +interface UploadDao { + @Query( + "SELECT _id FROM " + ProviderTableMeta.UPLOADS_TABLE_NAME + + " WHERE " + ProviderTableMeta.UPLOADS_STATUS + " = :status AND " + + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + " = :accountName AND _id IS NOT NULL" + ) + fun getAllIds(status: Int, accountName: String): List + + @Query( + "SELECT * FROM " + ProviderTableMeta.UPLOADS_TABLE_NAME + + " WHERE " + ProviderTableMeta._ID + " IN (:ids) AND " + + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + " = :accountName" + ) + fun getUploadsByIds(ids: LongArray, accountName: String): List + + @Query( + "SELECT * FROM ${ProviderTableMeta.UPLOADS_TABLE_NAME} " + + "WHERE ${ProviderTableMeta.UPLOADS_REMOTE_PATH} = :remotePath LIMIT 1" + ) + fun getByRemotePath(remotePath: String): UploadEntity? + + @Query( + "DELETE FROM ${ProviderTableMeta.UPLOADS_TABLE_NAME} " + + "WHERE ${ProviderTableMeta.UPLOADS_ACCOUNT_NAME} = :accountName " + + "AND ${ProviderTableMeta.UPLOADS_REMOTE_PATH} = :remotePath" + ) + fun deleteByAccountAndRemotePath(accountName: String, remotePath: String) + + @Query( + "SELECT * FROM " + ProviderTableMeta.UPLOADS_TABLE_NAME + + " WHERE " + ProviderTableMeta._ID + " = :id AND " + + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + " = :accountName " + + "LIMIT 1" + ) + fun getUploadById(id: Long, accountName: String): UploadEntity? + + @Insert(onConflict = OnConflictStrategy.Companion.REPLACE) + fun insertOrReplace(entity: UploadEntity): Long + + @Query( + "SELECT * FROM " + ProviderTableMeta.UPLOADS_TABLE_NAME + + " WHERE " + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + " = :accountName AND " + + ProviderTableMeta.UPLOADS_LOCAL_PATH + " = :localPath AND " + + ProviderTableMeta.UPLOADS_REMOTE_PATH + " = :remotePath " + + "LIMIT 1" + ) + fun getUploadByAccountAndPaths(accountName: String, localPath: String, remotePath: String): UploadEntity? + + @Query( + "UPDATE ${ProviderTableMeta.UPLOADS_TABLE_NAME} " + + "SET ${ProviderTableMeta.UPLOADS_STATUS} = :status " + + "WHERE ${ProviderTableMeta.UPLOADS_REMOTE_PATH} = :remotePath " + + "AND ${ProviderTableMeta.UPLOADS_ACCOUNT_NAME} = :accountName" + ) + suspend fun updateStatus(remotePath: String, accountName: String, status: Int): Int + + @Query( + """ + SELECT * FROM ${ProviderTableMeta.UPLOADS_TABLE_NAME} + WHERE ${ProviderTableMeta.UPLOADS_STATUS} = :status + AND (:nameCollisionPolicy IS NULL OR ${ProviderTableMeta.UPLOADS_NAME_COLLISION_POLICY} = :nameCollisionPolicy) +""" + ) + suspend fun getUploadsByStatus(status: Int, nameCollisionPolicy: Int? = null): List + + @Query( + """ + SELECT * FROM ${ProviderTableMeta.UPLOADS_TABLE_NAME} + WHERE ${ProviderTableMeta.UPLOADS_ACCOUNT_NAME} = :accountName + AND ${ProviderTableMeta.UPLOADS_STATUS} = :status + AND (:nameCollisionPolicy IS NULL OR ${ProviderTableMeta.UPLOADS_NAME_COLLISION_POLICY} = :nameCollisionPolicy) +""" + ) + suspend fun getUploadsByAccountNameAndStatus( + accountName: String, + status: Int, + nameCollisionPolicy: Int? = null + ): List +} diff --git a/app/src/main/java/com/nextcloud/client/database/entity/ArbitraryDataEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/ArbitraryDataEntity.kt index 851bd63..d996521 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/ArbitraryDataEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/ArbitraryDataEntity.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.database.entity diff --git a/app/src/main/java/com/nextcloud/client/database/entity/AssistantEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/AssistantEntity.kt new file mode 100644 index 0000000..4c13c25 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/entity/AssistantEntity.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.owncloud.android.db.ProviderMeta + +@Entity(tableName = ProviderMeta.ProviderTableMeta.ASSISTANT_TABLE_NAME) +data class AssistantEntity( + @PrimaryKey(autoGenerate = true) + val id: Long = 0L, + val accountName: String?, + val type: String?, + val status: String?, + val userId: String?, + val appId: String?, + val input: String? = null, + val output: String? = null, + val completionExpectedAt: Int? = null, + var progress: Int? = null, + val lastUpdated: Int? = null, + val scheduledAt: Int? = null, + val endedAt: Int? = null +) diff --git a/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt index 3b53e96..56f33b1 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.database.entity @@ -122,5 +122,29 @@ data class CapabilityEntity( @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT) val dropAccount: Int?, @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SECURITY_GUARD) - val securityGuard: Int? + val securityGuard: Int?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAME_CHARACTERS) + val forbiddenFileNameCharacters: Int?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAMES) + val forbiddenFileNames: Int?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_EXTENSIONS) + val forbiddenFileNameExtensions: Int?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_BASE_NAMES) + val forbiddenFilenameBaseNames: Int?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FILES_DOWNLOAD_LIMIT) + val filesDownloadLimit: Int?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_FILES_DOWNLOAD_LIMIT_DEFAULT) + val filesDownloadLimitDefault: Int?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_RECOMMENDATION) + val recommendation: Int?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_NOTES_FOLDER_PATH) + val notesFolderPath: String?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_DEFAULT_PERMISSIONS) + val defaultPermissions: Int?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_USER_STATUS_SUPPORTS_BUSY) + val userStatusSupportsBusy: Int?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_WINDOWS_COMPATIBLE_FILENAMES) + val isWCFEnabled: Int?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_HAS_VALID_SUBSCRIPTION) + val hasValidSubscription: Int? ) diff --git a/app/src/main/java/com/nextcloud/client/database/entity/ExternalLinkEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/ExternalLinkEntity.kt index dad89f3..03da555 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/ExternalLinkEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/ExternalLinkEntity.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.database.entity diff --git a/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt index bcfbf6e..175287b 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/FileEntity.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.database.entity @@ -50,7 +50,7 @@ data class FileEntity( @ColumnInfo(name = ProviderTableMeta.FILE_ETAG_ON_SERVER) val etagOnServer: String?, @ColumnInfo(name = ProviderTableMeta.FILE_SHARED_VIA_LINK) - val sharedViaLink: Int?, + var sharedViaLink: Int?, @ColumnInfo(name = ProviderTableMeta.FILE_PERMISSIONS) val permissions: String?, @ColumnInfo(name = ProviderTableMeta.FILE_REMOTE_ID) @@ -73,7 +73,7 @@ data class FileEntity( @ColumnInfo(name = ProviderTableMeta.FILE_ETAG_IN_CONFLICT) val etagInConflict: String?, @ColumnInfo(name = ProviderTableMeta.FILE_SHARED_WITH_SHAREE) - val sharedWithSharee: Int?, + var sharedWithSharee: Int?, @ColumnInfo(name = ProviderTableMeta.FILE_MOUNT_TYPE) val mountType: Int?, @ColumnInfo(name = ProviderTableMeta.FILE_HAS_PREVIEW) @@ -115,5 +115,11 @@ data class FileEntity( @ColumnInfo(name = ProviderTableMeta.FILE_METADATA_GPS) val metadataGPS: String?, @ColumnInfo(name = ProviderTableMeta.FILE_E2E_COUNTER) - val e2eCounter: Long? + val e2eCounter: Long?, + @ColumnInfo(name = ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP) + val internalTwoWaySync: Long?, + @ColumnInfo(name = ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_RESULT) + val internalTwoWaySyncResult: String?, + @ColumnInfo(name = ProviderTableMeta.FILE_UPLOADED) + val uploaded: Long? ) diff --git a/app/src/main/java/com/nextcloud/client/database/entity/FilesystemEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/FilesystemEntity.kt index 146d086..7247ee1 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/FilesystemEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/FilesystemEntity.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.database.entity diff --git a/app/src/main/java/com/nextcloud/client/database/entity/OfflineOperationEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/OfflineOperationEntity.kt new file mode 100644 index 0000000..1d5b151 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/entity/OfflineOperationEntity.kt @@ -0,0 +1,68 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.entity + +import android.content.Context +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.nextcloud.model.OfflineOperationType +import com.owncloud.android.R +import com.owncloud.android.db.ProviderMeta.ProviderTableMeta + +@Entity(tableName = ProviderTableMeta.OFFLINE_OPERATION_TABLE_NAME) +data class OfflineOperationEntity( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = ProviderTableMeta._ID) + val id: Int? = null, + + @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_PARENT_OC_FILE_ID) + var parentOCFileId: Long? = null, + + @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_PATH) + var path: String? = null, + + @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_TYPE) + var type: OfflineOperationType? = null, + + @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_FILE_NAME) + var filename: String? = null, + + @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_CREATED_AT) + var createdAt: Long? = null, + + @ColumnInfo(name = ProviderTableMeta.OFFLINE_OPERATION_MODIFIED_AT) + var modifiedAt: Long? = null +) { + fun isRenameOrRemove(): Boolean = + (type is OfflineOperationType.RenameFile || type is OfflineOperationType.RemoveFile) + + fun isCreate(): Boolean = (type is OfflineOperationType.CreateFile || type is OfflineOperationType.CreateFolder) + + fun getConflictText(context: Context): String { + val resId = when (type) { + is OfflineOperationType.RemoveFile -> { + R.string.offline_operations_worker_notification_remove_conflict_text + } + + is OfflineOperationType.RenameFile -> { + R.string.offline_operations_worker_notification_rename_conflict_text + } + + is OfflineOperationType.CreateFile -> { + R.string.offline_operations_worker_notification_create_file_conflict_text + } + + else -> { + R.string.offline_operations_worker_notification_create_folder_conflict_text + } + } + + return context.getString(resId, filename) + } +} diff --git a/app/src/main/java/com/nextcloud/client/database/entity/RecommendedFileEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/RecommendedFileEntity.kt new file mode 100644 index 0000000..6501797 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/entity/RecommendedFileEntity.kt @@ -0,0 +1,72 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.nextcloud.android.lib.resources.recommendations.Recommendation +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.db.ProviderMeta.ProviderTableMeta + +@Entity(tableName = ProviderTableMeta.RECOMMENDED_FILE_TABLE_NAME) +data class RecommendedFileEntity( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = ProviderTableMeta._ID) + val id: Long, + + @ColumnInfo(name = ProviderTableMeta.RECOMMENDED_FILE_NAME) + val name: String, + + @ColumnInfo(name = ProviderTableMeta.RECOMMENDED_FILE_DIRECTORY) + val directory: String, + + @ColumnInfo(name = ProviderTableMeta.RECOMMENDED_FILE_EXTENSIONS) + val extension: String, + + @ColumnInfo(name = ProviderTableMeta.RECOMMENDED_FILE_MIME_TYPE) + val mimeType: String, + + @ColumnInfo(name = ProviderTableMeta.RECOMMENDED_FILE_HAS_PREVIEW) + val hasPreview: Boolean, + + @ColumnInfo(name = ProviderTableMeta.RECOMMENDED_FILE_REASON) + val reason: String, + + @ColumnInfo(name = ProviderTableMeta.RECOMMENDED_TIMESTAMP) + val timestamp: Long, + + @ColumnInfo(name = ProviderTableMeta.RECOMMENDED_FILE_ACCOUNT_NAME) + val accountName: String? +) + +fun ArrayList.toEntity(accountName: String): List = this.map { recommendation -> + RecommendedFileEntity( + id = recommendation.id, + name = recommendation.name, + directory = recommendation.directory, + extension = recommendation.extension, + mimeType = recommendation.mimeType, + hasPreview = recommendation.hasPreview, + reason = recommendation.reason, + timestamp = recommendation.timestamp, + accountName = accountName + ) +} + +fun List.toOCFile(storageManager: FileDataStorageManager): ArrayList = + mapNotNull { entity -> + entity.id.let { + storageManager.getFileByLocalId(it).apply { + this?.reason = entity.reason + this?.setIsRecommendedFile(true) + } + } + } + .toCollection(ArrayList()) diff --git a/app/src/main/java/com/nextcloud/client/database/entity/ShareEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/ShareEntity.kt index caeb875..ad5005e 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/ShareEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/ShareEntity.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.database.entity @@ -54,5 +54,11 @@ data class ShareEntity( @ColumnInfo(name = ProviderTableMeta.OCSHARES_SHARE_LINK) val shareLink: String?, @ColumnInfo(name = ProviderTableMeta.OCSHARES_SHARE_LABEL) - val shareLabel: String? + val shareLabel: String?, + @ColumnInfo(name = ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_LIMIT) + val downloadLimitLimit: Int?, + @ColumnInfo(name = ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_COUNT) + val downloadLimitCount: Int?, + @ColumnInfo(name = ProviderTableMeta.OCSHARES_ATTRIBUTES) + val attributes: String? ) diff --git a/app/src/main/java/com/nextcloud/client/database/entity/SyncedFolderEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/SyncedFolderEntity.kt index 8a4298c..18b0911 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/SyncedFolderEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/SyncedFolderEntity.kt @@ -3,13 +3,16 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.database.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import com.nextcloud.client.preferences.SubFolderRule +import com.owncloud.android.datamodel.MediaFolderType +import com.owncloud.android.datamodel.SyncedFolder import com.owncloud.android.db.ProviderMeta.ProviderTableMeta @Entity(tableName = ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME) @@ -50,3 +53,40 @@ data class SyncedFolderEntity( @ColumnInfo(name = ProviderTableMeta.SYNCED_FOLDER_LAST_SCAN_TIMESTAMP_MS) val lastScanTimestampMs: Long? ) + +fun SyncedFolderEntity.toSyncedFolder(): SyncedFolder = SyncedFolder( + // id + (this.id ?: SyncedFolder.UNPERSISTED_ID).toLong(), + // localPath + this.localPath ?: "", + // remotePath + this.remotePath ?: "", + // wifiOnly + this.wifiOnly == 1, + // chargingOnly + this.chargingOnly == 1, + // existing + this.existing == 1, + // subfolderByDate + this.subfolderByDate == 1, + // account + this.account ?: "", + // uploadAction + this.uploadAction ?: 0, + // nameCollisionPolicy + this.nameCollisionPolicy ?: 0, + // enabled + this.enabled == 1, + // timestampMs + (this.enabledTimestampMs ?: SyncedFolder.EMPTY_ENABLED_TIMESTAMP_MS).toLong(), + // type + MediaFolderType.getById(this.type ?: MediaFolderType.CUSTOM.id), + // hidden + this.hidden == 1, + // subFolderRule + this.subFolderRule?.let { SubFolderRule.entries[it] }, + // excludeHidden + this.excludeHidden == 1, + // lastScanTimestampMs + this.lastScanTimestampMs ?: SyncedFolder.NOT_SCANNED_YET +) diff --git a/app/src/main/java/com/nextcloud/client/database/entity/UploadEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/UploadEntity.kt index e4c59c1..ec9042c 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/UploadEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/UploadEntity.kt @@ -1,16 +1,23 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.database.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import com.nextcloud.utils.autoRename.AutoRename +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.db.OCUpload import com.owncloud.android.db.ProviderMeta.ProviderTableMeta +import com.owncloud.android.db.UploadResult +import com.owncloud.android.files.services.NameCollisionPolicy +import com.owncloud.android.lib.resources.status.OCCapability @Entity(tableName = ProviderTableMeta.UPLOADS_TABLE_NAME) data class UploadEntity( @@ -48,3 +55,57 @@ data class UploadEntity( @ColumnInfo(name = ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN) val folderUnlockToken: String? ) + +fun UploadEntity.toOCUpload(capability: OCCapability? = null): OCUpload { + val localPath = localPath + var remotePath = remotePath + if (capability != null && remotePath != null) { + remotePath = AutoRename.rename(remotePath, capability) + } + val upload = OCUpload(localPath, remotePath, accountName) + + fileSize?.let { upload.fileSize = it } + id?.let { upload.uploadId = it.toLong() } + status?.let { upload.uploadStatus = UploadsStorageManager.UploadStatus.fromValue(it) } + localBehaviour?.let { upload.localAction = it } + nameCollisionPolicy?.let { upload.nameCollisionPolicy = NameCollisionPolicy.deserialize(it) } + isCreateRemoteFolder?.let { upload.isCreateRemoteFolder = it == 1 } + uploadEndTimestamp?.let { upload.uploadEndTimestamp = it.toLong() } + lastResult?.let { upload.lastResult = UploadResult.fromValue(it) } + createdBy?.let { upload.createdBy = it } + isWifiOnly?.let { upload.isUseWifiOnly = it == 1 } + isWhileChargingOnly?.let { upload.isWhileChargingOnly = it == 1 } + folderUnlockToken?.let { upload.folderUnlockToken = it } + + return upload +} + +fun OCUpload.toUploadEntity(): UploadEntity { + val id = if (uploadId == -1L) { + // needed for the insert new records to the db so that insert DAO function returns new generated id + null + } else { + uploadId + } + + return UploadEntity( + id = id?.toInt(), + localPath = localPath, + remotePath = remotePath, + accountName = accountName, + fileSize = fileSize, + status = uploadStatus?.value, + localBehaviour = localAction, + nameCollisionPolicy = nameCollisionPolicy?.serialize(), + isCreateRemoteFolder = if (isCreateRemoteFolder) 1 else 0, + + // uploadEndTimestamp may overflow max int capacity since it is conversion from long to int. coerceAtMost needed + uploadEndTimestamp = uploadEndTimestamp.coerceAtMost(Int.MAX_VALUE.toLong()).toInt(), + lastResult = lastResult?.value, + createdBy = createdBy, + isWifiOnly = if (isUseWifiOnly) 1 else 0, + isWhileChargingOnly = if (isWhileChargingOnly) 1 else 0, + folderUnlockToken = folderUnlockToken, + uploadTime = null + ) +} diff --git a/app/src/main/java/com/nextcloud/client/database/entity/VirtualEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/VirtualEntity.kt index a8eb01f..d8c7efe 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/VirtualEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/VirtualEntity.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.database.entity diff --git a/app/src/main/java/com/nextcloud/client/database/migrations/DatabaseMigrationUtil.kt b/app/src/main/java/com/nextcloud/client/database/migrations/DatabaseMigrationUtil.kt index 225aea8..b520b47 100644 --- a/app/src/main/java/com/nextcloud/client/database/migrations/DatabaseMigrationUtil.kt +++ b/app/src/main/java/com/nextcloud/client/database/migrations/DatabaseMigrationUtil.kt @@ -3,12 +3,14 @@ * * SPDX-FileCopyrightText: 2023 Álvaro Brey * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.database.migrations +import androidx.room.DeleteColumn import androidx.room.migration.AutoMigrationSpec import androidx.sqlite.db.SupportSQLiteDatabase +import com.nextcloud.client.database.migrations.model.SQLiteColumnType object DatabaseMigrationUtil { @@ -17,6 +19,32 @@ object DatabaseMigrationUtil { const val TYPE_INTEGER_PRIMARY_KEY = "INTEGER PRIMARY KEY" const val KEYWORD_NOT_NULL = "NOT NULL" + fun addColumnIfNotExists( + db: SupportSQLiteDatabase, + tableName: String, + columnName: String, + columnType: SQLiteColumnType + ) { + val cursor = db.query("PRAGMA table_info($tableName)") + var columnExists = false + + while (cursor.moveToNext()) { + val nameIndex = cursor.getColumnIndex("name") + if (nameIndex != -1) { + val existingColumnName = cursor.getString(nameIndex) + if (existingColumnName == columnName) { + columnExists = true + break + } + } + } + cursor.close() + + if (!columnExists) { + db.execSQL("ALTER TABLE $tableName ADD COLUMN `$columnName` ${columnType.value}") + } + } + /** * Utility method to add or remove columns from a table * @@ -46,11 +74,7 @@ object DatabaseMigrationUtil { /** * Utility method to create a new table with the given columns */ - private fun createNewTable( - database: SupportSQLiteDatabase, - newTableName: String, - columns: Map - ) { + private fun createNewTable(database: SupportSQLiteDatabase, newTableName: String, columns: Map) { val columnsString = columns.entries.joinToString(",") { "${it.key} ${it.value}" } database.execSQL("CREATE TABLE $newTableName ($columnsString)") } @@ -80,11 +104,7 @@ object DatabaseMigrationUtil { /** * Utility method to replace an old table with a new one, essentially deleting the old one and renaming the new one */ - private fun replaceTable( - database: SupportSQLiteDatabase, - tableName: String, - newTableTempName: String - ) { + private fun replaceTable(database: SupportSQLiteDatabase, tableName: String, newTableTempName: String) { database.execSQL("DROP TABLE $tableName") database.execSQL("ALTER TABLE $newTableTempName RENAME TO $tableName") } @@ -98,4 +118,12 @@ object DatabaseMigrationUtil { super.onPostMigrate(db) } } + + @DeleteColumn.Entries( + DeleteColumn( + tableName = "offline_operations", + columnName = "offline_operations_parent_path" + ) + ) + class DeleteColumnSpec : AutoMigrationSpec } diff --git a/app/src/main/java/com/nextcloud/client/database/migrations/LegacyMigration.kt b/app/src/main/java/com/nextcloud/client/database/migrations/LegacyMigration.kt index 9d025b9..d975cb4 100644 --- a/app/src/main/java/com/nextcloud/client/database/migrations/LegacyMigration.kt +++ b/app/src/main/java/com/nextcloud/client/database/migrations/LegacyMigration.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.database.migrations diff --git a/app/src/main/java/com/nextcloud/client/database/migrations/LegacyMigrationHelper.java b/app/src/main/java/com/nextcloud/client/database/migrations/LegacyMigrationHelper.java index ec93cd6..4cbd78a 100644 --- a/app/src/main/java/com/nextcloud/client/database/migrations/LegacyMigrationHelper.java +++ b/app/src/main/java/com/nextcloud/client/database/migrations/LegacyMigrationHelper.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.database.migrations; diff --git a/app/src/main/java/com/nextcloud/client/database/migrations/Migration67to68.kt b/app/src/main/java/com/nextcloud/client/database/migrations/Migration67to68.kt index b7b10a6..5e7ad66 100644 --- a/app/src/main/java/com/nextcloud/client/database/migrations/Migration67to68.kt +++ b/app/src/main/java/com/nextcloud/client/database/migrations/Migration67to68.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Álvaro Brey * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.database.migrations diff --git a/app/src/main/java/com/nextcloud/client/database/migrations/Migration88to89.kt b/app/src/main/java/com/nextcloud/client/database/migrations/Migration88to89.kt new file mode 100644 index 0000000..3034ae2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/migrations/Migration88to89.kt @@ -0,0 +1,31 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.nextcloud.client.database.migrations.model.SQLiteColumnType +import com.owncloud.android.db.ProviderMeta.ProviderTableMeta + +@Suppress("MagicNumber") +val MIGRATION_88_89 = object : Migration(88, 89) { + override fun migrate(database: SupportSQLiteDatabase) { + DatabaseMigrationUtil.addColumnIfNotExists( + database, + ProviderTableMeta.FILE_TABLE_NAME, + ProviderTableMeta.FILE_UPLOADED, + SQLiteColumnType.INTEGER_DEFAULT_NULL + ) + DatabaseMigrationUtil.addColumnIfNotExists( + database, + ProviderTableMeta.CAPABILITIES_TABLE_NAME, + ProviderTableMeta.CAPABILITIES_NOTES_FOLDER_PATH, + SQLiteColumnType.TEXT_DEFAULT_NULL + ) + } +} diff --git a/app/src/main/java/com/nextcloud/client/database/migrations/RoomMigration.kt b/app/src/main/java/com/nextcloud/client/database/migrations/RoomMigration.kt index 2da2ff0..1d41116 100644 --- a/app/src/main/java/com/nextcloud/client/database/migrations/RoomMigration.kt +++ b/app/src/main/java/com/nextcloud/client/database/migrations/RoomMigration.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.database.migrations diff --git a/app/src/main/java/com/nextcloud/client/database/migrations/model/SQLiteColumnType.kt b/app/src/main/java/com/nextcloud/client/database/migrations/model/SQLiteColumnType.kt new file mode 100644 index 0000000..96777ca --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/migrations/model/SQLiteColumnType.kt @@ -0,0 +1,13 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.migrations.model + +enum class SQLiteColumnType(val value: String) { + INTEGER_DEFAULT_NULL("INTEGER DEFAULT NULL"), + TEXT_DEFAULT_NULL("TEXT DEFAULT NULL") +} diff --git a/app/src/main/java/com/nextcloud/client/database/typeAdapter/OfflineOperationTypeAdapter.kt b/app/src/main/java/com/nextcloud/client/database/typeAdapter/OfflineOperationTypeAdapter.kt new file mode 100644 index 0000000..bd1ece7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/typeAdapter/OfflineOperationTypeAdapter.kt @@ -0,0 +1,96 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.typeAdapter + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonSerializationContext +import com.google.gson.JsonSerializer +import com.nextcloud.model.OfflineOperationRawType +import com.nextcloud.model.OfflineOperationType + +import java.lang.reflect.Type + +class OfflineOperationTypeAdapter : + JsonSerializer, + JsonDeserializer { + + override fun serialize( + src: OfflineOperationType?, + typeOfSrc: Type?, + context: JsonSerializationContext? + ): JsonElement { + val jsonObject = JsonObject() + jsonObject.addProperty("type", src?.javaClass?.simpleName) + when (src) { + is OfflineOperationType.CreateFolder -> { + jsonObject.addProperty("type", src.type) + jsonObject.addProperty("path", src.path) + } + + is OfflineOperationType.CreateFile -> { + jsonObject.addProperty("type", src.type) + jsonObject.addProperty("localPath", src.localPath) + jsonObject.addProperty("remotePath", src.remotePath) + jsonObject.addProperty("mimeType", src.mimeType) + } + + is OfflineOperationType.RenameFile -> { + jsonObject.addProperty("type", src.type) + jsonObject.addProperty("ocFileId", src.ocFileId) + jsonObject.addProperty("newName", src.newName) + } + + is OfflineOperationType.RemoveFile -> { + jsonObject.addProperty("type", src.type) + jsonObject.addProperty("path", src.path) + } + + null -> Unit + } + + return jsonObject + } + + override fun deserialize( + json: JsonElement?, + typeOfT: Type?, + context: JsonDeserializationContext? + ): OfflineOperationType? { + val jsonObject = json?.asJsonObject ?: return null + val type = jsonObject.get("type")?.asString + return when (type) { + OfflineOperationRawType.CreateFolder.name -> OfflineOperationType.CreateFolder( + jsonObject.get("type").asString, + jsonObject.get("path").asString + ) + + OfflineOperationRawType.CreateFile.name -> OfflineOperationType.CreateFile( + jsonObject.get("type").asString, + jsonObject.get("localPath").asString, + jsonObject.get("remotePath").asString, + jsonObject.get("mimeType").asString + ) + + OfflineOperationRawType.RenameFile.name -> OfflineOperationType.RenameFile( + jsonObject.get("type").asString, + jsonObject.get("ocFileId").asLong, + jsonObject.get("newName").asString + ) + + OfflineOperationRawType.RemoveFile.name -> OfflineOperationType.RemoveFile( + jsonObject.get("type").asString, + jsonObject.get("path").asString + ) + + else -> null + } + } +} diff --git a/app/src/main/java/com/nextcloud/client/database/typeConverter/OfflineOperationTypeConverter.kt b/app/src/main/java/com/nextcloud/client/database/typeConverter/OfflineOperationTypeConverter.kt new file mode 100644 index 0000000..3436623 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/database/typeConverter/OfflineOperationTypeConverter.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.database.typeConverter + +import androidx.room.ProvidedTypeConverter +import androidx.room.TypeConverter +import com.google.gson.Gson +import com.nextcloud.model.OfflineOperationType +import com.google.gson.GsonBuilder +import com.nextcloud.client.database.typeAdapter.OfflineOperationTypeAdapter + +@ProvidedTypeConverter +class OfflineOperationTypeConverter { + + private val gson: Gson = GsonBuilder() + .registerTypeAdapter(OfflineOperationType::class.java, OfflineOperationTypeAdapter()) + .create() + + @TypeConverter + fun fromOfflineOperationType(type: OfflineOperationType?): String? = gson.toJson(type) + + @TypeConverter + fun toOfflineOperationType(type: String?): OfflineOperationType? = + gson.fromJson(type, OfflineOperationType::class.java) +} diff --git a/app/src/main/java/com/nextcloud/client/device/BatteryStatus.kt b/app/src/main/java/com/nextcloud/client/device/BatteryStatus.kt index 8fee2a8..a941026 100644 --- a/app/src/main/java/com/nextcloud/client/device/BatteryStatus.kt +++ b/app/src/main/java/com/nextcloud/client/device/BatteryStatus.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.device diff --git a/app/src/main/java/com/nextcloud/client/device/DeviceInfo.kt b/app/src/main/java/com/nextcloud/client/device/DeviceInfo.kt index 6dc7273..4888930 100644 --- a/app/src/main/java/com/nextcloud/client/device/DeviceInfo.kt +++ b/app/src/main/java/com/nextcloud/client/device/DeviceInfo.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.device @@ -12,11 +12,10 @@ import android.os.Build import java.util.Locale class DeviceInfo { - val vendor: String = Build.MANUFACTURER.toLowerCase(Locale.ROOT) + val vendor: String = Build.MANUFACTURER.lowercase(Locale.ROOT) val apiLevel: Int = Build.VERSION.SDK_INT val androidVersion = Build.VERSION.RELEASE - fun hasCamera(context: Context): Boolean { - return context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) - } + fun hasCamera(context: Context): Boolean = + context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) } diff --git a/app/src/main/java/com/nextcloud/client/device/DeviceModule.kt b/app/src/main/java/com/nextcloud/client/device/DeviceModule.kt index 88fd1ee..139fbe6 100644 --- a/app/src/main/java/com/nextcloud/client/device/DeviceModule.kt +++ b/app/src/main/java/com/nextcloud/client/device/DeviceModule.kt @@ -2,15 +2,14 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-FileCopyrightText: 2019 Tobias Kaminsky + * SPDX-FileCopyrightText: 2019 Tobias Kaminsky * SPDX-FileCopyrightText: 2019 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.device import android.content.Context import android.os.PowerManager -import com.nextcloud.client.preferences.AppPreferences import dagger.Module import dagger.Provides @@ -18,13 +17,11 @@ import dagger.Provides class DeviceModule { @Provides - fun powerManagementService(context: Context, preferences: AppPreferences): PowerManagementService { + fun powerManagementService(context: Context): PowerManagementService { val platformPowerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager return PowerManagementServiceImpl( context = context, - platformPowerManager = platformPowerManager, - deviceInfo = DeviceInfo(), - preferences = preferences + platformPowerManager = platformPowerManager ) } } diff --git a/app/src/main/java/com/nextcloud/client/device/PowerManagementService.kt b/app/src/main/java/com/nextcloud/client/device/PowerManagementService.kt index 663df6b..730ca4e 100644 --- a/app/src/main/java/com/nextcloud/client/device/PowerManagementService.kt +++ b/app/src/main/java/com/nextcloud/client/device/PowerManagementService.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.device @@ -21,14 +21,6 @@ interface PowerManagementService { */ val isPowerSavingEnabled: Boolean - /** - * Checks if the device vendor requires power saving - * exclusion workaround. - * - * @return true if workaround is required, false otherwise - */ - val isPowerSavingExclusionAvailable: Boolean - /** * Checks current battery status using platform [android.os.BatteryManager] */ diff --git a/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt b/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt index 3a060cf..3c8d56c 100644 --- a/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt +++ b/app/src/main/java/com/nextcloud/client/device/PowerManagementServiceImpl.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.device @@ -11,46 +11,27 @@ import android.content.Intent import android.content.IntentFilter import android.os.BatteryManager import android.os.PowerManager -import com.nextcloud.client.preferences.AppPreferences -import com.nextcloud.client.preferences.AppPreferencesImpl import com.nextcloud.utils.extensions.registerBroadcastReceiver import com.owncloud.android.datamodel.ReceiverFlag internal class PowerManagementServiceImpl( private val context: Context, - private val platformPowerManager: PowerManager, - private val preferences: AppPreferences, - private val deviceInfo: DeviceInfo = DeviceInfo() + private val platformPowerManager: PowerManager ) : PowerManagementService { companion object { - /** - * Vendors on this list use aggressive power saving methods that might - * break application experience. - */ - val OVERLY_AGGRESSIVE_POWER_SAVING_VENDORS = setOf("samsung", "huawei", "xiaomi") - @JvmStatic fun fromContext(context: Context): PowerManagementServiceImpl { val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager - val preferences = AppPreferencesImpl.fromContext(context) - - return PowerManagementServiceImpl(context, powerManager, preferences, DeviceInfo()) + return PowerManagementServiceImpl(context, powerManager) } } override val isPowerSavingEnabled: Boolean get() { - if (preferences.isPowerCheckDisabled) { - return false - } - return platformPowerManager.isPowerSaveMode } - override val isPowerSavingExclusionAvailable: Boolean - get() = deviceInfo.vendor in OVERLY_AGGRESSIVE_POWER_SAVING_VENDORS - @Suppress("MagicNumber") // 100% is 100, we're not doing Cobol override val battery: BatteryStatus get() { diff --git a/app/src/main/java/com/nextcloud/client/di/ActivityInjector.kt b/app/src/main/java/com/nextcloud/client/di/ActivityInjector.kt index a49c566..876c5d0 100644 --- a/app/src/main/java/com/nextcloud/client/di/ActivityInjector.kt +++ b/app/src/main/java/com/nextcloud/client/di/ActivityInjector.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.di diff --git a/app/src/main/java/com/nextcloud/client/di/AppComponent.java b/app/src/main/java/com/nextcloud/client/di/AppComponent.java index 528ee63..8e1f599 100644 --- a/app/src/main/java/com/nextcloud/client/di/AppComponent.java +++ b/app/src/main/java/com/nextcloud/client/di/AppComponent.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.di; @@ -16,7 +16,11 @@ import com.nextcloud.client.device.DeviceModule; import com.nextcloud.client.integrations.IntegrationsModule; import com.nextcloud.client.jobs.JobsModule; import com.nextcloud.client.jobs.download.FileDownloadHelper; +import com.nextcloud.client.jobs.offlineOperations.receiver.OfflineOperationReceiver; +import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorkerReceiver; +import com.nextcloud.client.jobs.upload.FileUploadBroadcastReceiver; import com.nextcloud.client.jobs.upload.FileUploadHelper; +import com.nextcloud.client.media.BackgroundPlayerService; import com.nextcloud.client.network.NetworkModule; import com.nextcloud.client.onboarding.OnboardingModule; import com.nextcloud.client.preferences.PreferencesModule; @@ -27,6 +31,8 @@ import com.owncloud.android.ui.whatsnew.ProgressIndicator; import javax.inject.Singleton; +import androidx.annotation.OptIn; +import androidx.media3.common.util.UnstableApi; import dagger.BindsInstance; import dagger.Component; import dagger.android.support.AndroidSupportInjectionModule; @@ -46,7 +52,7 @@ import dagger.android.support.AndroidSupportInjectionModule; ThemeModule.class, DatabaseModule.class, DispatcherModule.class, - VariantModule.class + VariantModule.class, }) @Singleton public interface AppComponent { @@ -55,6 +61,9 @@ public interface AppComponent { void inject(MediaControlView mediaControlView); + @OptIn(markerClass = UnstableApi.class) + void inject(BackgroundPlayerService backgroundPlayerService); + void inject(ThemeableSwitchPreference switchPreference); void inject(FileUploadHelper fileUploadHelper); @@ -63,6 +72,12 @@ public interface AppComponent { void inject(ProgressIndicator progressIndicator); + void inject(FileUploadBroadcastReceiver fileUploadBroadcastReceiver); + + void inject(OfflineOperationReceiver offlineOperationReceiver); + + void inject(FolderDownloadWorkerReceiver folderDownloadWorkerReceiver); + @Component.Builder interface Builder { @BindsInstance diff --git a/app/src/main/java/com/nextcloud/client/di/AppModule.java b/app/src/main/java/com/nextcloud/client/di/AppModule.java index eb79b07..32a0150 100644 --- a/app/src/main/java/com/nextcloud/client/di/AppModule.java +++ b/app/src/main/java/com/nextcloud/client/di/AppModule.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.di; @@ -28,6 +28,7 @@ import com.nextcloud.client.core.ClockImpl; import com.nextcloud.client.core.ThreadPoolAsyncRunner; import com.nextcloud.client.database.dao.ArbitraryDataDao; import com.nextcloud.client.device.DeviceInfo; +import com.nextcloud.client.jobs.operation.FileOperationHelper; import com.nextcloud.client.logger.FileLogHandler; import com.nextcloud.client.logger.Logger; import com.nextcloud.client.logger.LoggerImpl; @@ -55,6 +56,7 @@ import com.owncloud.android.ui.activities.data.activities.RemoteActivitiesReposi import com.owncloud.android.ui.activities.data.files.FilesRepository; import com.owncloud.android.ui.activities.data.files.FilesServiceApiImpl; import com.owncloud.android.ui.activities.data.files.RemoteFilesRepository; +import com.owncloud.android.ui.dialog.setupEncryption.CertificateValidator; import com.owncloud.android.utils.theme.ViewThemeUtils; import org.greenrobot.eventbus.EventBus; @@ -249,10 +251,21 @@ class AppModule { return new PassCodeManager(preferences, clock); } + @Provides + FileOperationHelper fileOperationHelper(CurrentAccountProvider currentAccountProvider, Context context) { + return new FileOperationHelper(currentAccountProvider.getUser(), context, fileDataStorageManager(currentAccountProvider, context)); + } + @Provides @Singleton UsersAndGroupsSearchConfig userAndGroupSearchConfig() { return new UsersAndGroupsSearchConfig(); } + + @Provides + @Singleton + CertificateValidator certificateValidator() { + return new CertificateValidator(); + } } 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 60346b2..dc7194f 100644 --- a/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java +++ b/app/src/main/java/com/nextcloud/client/di/ComponentsModule.java @@ -1,8 +1,9 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2024 TSI-mc * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.di; @@ -17,6 +18,7 @@ import com.nextcloud.client.jobs.transfer.FileTransferService; import com.nextcloud.client.jobs.upload.FileUploadHelper; import com.nextcloud.client.logger.ui.LogsActivity; import com.nextcloud.client.logger.ui.LogsViewModel; +import com.nextcloud.client.media.BackgroundPlayerService; import com.nextcloud.client.media.PlayerService; import com.nextcloud.client.migrations.Migrations; import com.nextcloud.client.onboarding.FirstRunActivity; @@ -24,11 +26,15 @@ import com.nextcloud.client.onboarding.WhatsNewActivity; import com.nextcloud.client.widget.DashboardWidgetConfigurationActivity; import com.nextcloud.client.widget.DashboardWidgetProvider; import com.nextcloud.client.widget.DashboardWidgetService; +import com.nextcloud.receiver.NetworkChangeReceiver; import com.nextcloud.ui.ChooseAccountDialogFragment; +import com.nextcloud.ui.ChooseStorageLocationDialogFragment; import com.nextcloud.ui.ImageDetailFragment; -import com.nextcloud.ui.SetStatusDialogFragment; +import com.nextcloud.ui.SetOnlineStatusBottomSheet; +import com.nextcloud.ui.SetStatusMessageBottomSheet; import com.nextcloud.ui.composeActivity.ComposeActivity; import com.nextcloud.ui.fileactions.FileActionsBottomSheet; +import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsBottomSheet; import com.nmc.android.ui.LauncherActivity; import com.owncloud.android.MainApp; import com.owncloud.android.authentication.AuthenticatorActivity; @@ -54,6 +60,7 @@ import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.activity.FilePickerActivity; import com.owncloud.android.ui.activity.FolderPickerActivity; +import com.owncloud.android.ui.activity.InternalTwoWaySyncActivity; import com.owncloud.android.ui.activity.ManageAccountsActivity; import com.owncloud.android.ui.activity.ManageSpaceActivity; import com.owncloud.android.ui.activity.NotificationsActivity; @@ -83,16 +90,16 @@ import com.owncloud.android.ui.dialog.LocalStoragePathPickerDialogFragment; import com.owncloud.android.ui.dialog.MultipleAccountsDialog; import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment; import com.owncloud.android.ui.dialog.RenameFileDialogFragment; -import com.owncloud.android.ui.dialog.RenamePublicShareDialogFragment; import com.owncloud.android.ui.dialog.SendFilesDialog; import com.owncloud.android.ui.dialog.SendShareDialog; -import com.owncloud.android.ui.dialog.SetupEncryptionDialogFragment; import com.owncloud.android.ui.dialog.SharePasswordDialogFragment; import com.owncloud.android.ui.dialog.SortingOrderDialogFragment; import com.owncloud.android.ui.dialog.SslUntrustedCertDialog; import com.owncloud.android.ui.dialog.StoragePermissionDialogFragment; import com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragment; import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment; +import com.owncloud.android.ui.dialog.TermsOfServiceDialog; +import com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment; import com.owncloud.android.ui.fragment.ExtendedListFragment; import com.owncloud.android.ui.fragment.FeatureFragment; import com.owncloud.android.ui.fragment.FileDetailActivitiesFragment; @@ -121,6 +128,8 @@ import com.owncloud.android.ui.preview.PreviewTextStringFragment; import com.owncloud.android.ui.preview.pdf.PreviewPdfFragment; import com.owncloud.android.ui.trashbin.TrashbinActivity; +import androidx.annotation.OptIn; +import androidx.media3.common.util.UnstableApi; import dagger.Module; import dagger.android.ContributesAndroidInjector; @@ -219,6 +228,9 @@ abstract class ComponentsModule { @ContributesAndroidInjector abstract TrashbinActivity trashbinActivity(); + @ContributesAndroidInjector + abstract TrashbinFileActionsBottomSheet trashbinFileActionsBottomSheet(); + @ContributesAndroidInjector abstract UploadFilesActivity uploadFilesActivity(); @@ -289,7 +301,7 @@ abstract class ComponentsModule { abstract ChooseAccountDialogFragment chooseAccountDialogFragment(); @ContributesAndroidInjector - abstract SetStatusDialogFragment setStatusDialogFragment(); + abstract SetOnlineStatusBottomSheet setOnlineStatusBottomSheet(); @ContributesAndroidInjector abstract PreviewTextFileFragment previewTextFileFragment(); @@ -312,6 +324,9 @@ abstract class ComponentsModule { @ContributesAndroidInjector abstract BootupBroadcastReceiver bootupBroadcastReceiver(); + @ContributesAndroidInjector + abstract NetworkChangeReceiver networkChangeReceiver(); + @ContributesAndroidInjector abstract NotificationWork.NotificationReceiver notificationWorkBroadcastReceiver(); @@ -399,15 +414,15 @@ abstract class ComponentsModule { @ContributesAndroidInjector abstract RemoveFilesDialogFragment removeFilesDialogFragment(); - @ContributesAndroidInjector - abstract RenamePublicShareDialogFragment renamePublicShareDialogFragment(); - @ContributesAndroidInjector abstract SendShareDialog sendShareDialog(); @ContributesAndroidInjector abstract SetupEncryptionDialogFragment setupEncryptionDialogFragment(); + @ContributesAndroidInjector + abstract ChooseStorageLocationDialogFragment chooseStorageLocationDialogFragment(); + @ContributesAndroidInjector abstract SharePasswordDialogFragment sharePasswordDialogFragment(); @@ -476,4 +491,18 @@ abstract class ComponentsModule { @ContributesAndroidInjector abstract TestJob testJob(); + + @ContributesAndroidInjector + abstract InternalTwoWaySyncActivity internalTwoWaySyncActivity(); + + + @OptIn(markerClass = UnstableApi.class) + @ContributesAndroidInjector + abstract BackgroundPlayerService backgroundPlayerService(); + + @ContributesAndroidInjector + abstract TermsOfServiceDialog termsOfServiceDialog(); + + @ContributesAndroidInjector + abstract SetStatusMessageBottomSheet setStatusMessageBottomSheet(); } diff --git a/app/src/main/java/com/nextcloud/client/di/DispatcherModule.kt b/app/src/main/java/com/nextcloud/client/di/DispatcherModule.kt index 7f2679b..ab15cdf 100644 --- a/app/src/main/java/com/nextcloud/client/di/DispatcherModule.kt +++ b/app/src/main/java/com/nextcloud/client/di/DispatcherModule.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.di diff --git a/app/src/main/java/com/nextcloud/client/di/FragmentInjector.kt b/app/src/main/java/com/nextcloud/client/di/FragmentInjector.kt index 4622095..5d76e73 100644 --- a/app/src/main/java/com/nextcloud/client/di/FragmentInjector.kt +++ b/app/src/main/java/com/nextcloud/client/di/FragmentInjector.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.di @@ -12,11 +12,7 @@ import androidx.fragment.app.FragmentManager import dagger.android.support.AndroidSupportInjection internal class FragmentInjector : FragmentManager.FragmentLifecycleCallbacks() { - override fun onFragmentPreAttached( - fragmentManager: FragmentManager, - fragment: Fragment, - context: Context - ) { + override fun onFragmentPreAttached(fragmentManager: FragmentManager, fragment: Fragment, context: Context) { super.onFragmentPreAttached(fragmentManager, fragment, context) if (fragment is Injectable) { try { diff --git a/app/src/main/java/com/nextcloud/client/di/Injectable.java b/app/src/main/java/com/nextcloud/client/di/Injectable.java index 28ab315..2edc275 100644 --- a/app/src/main/java/com/nextcloud/client/di/Injectable.java +++ b/app/src/main/java/com/nextcloud/client/di/Injectable.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.di; diff --git a/app/src/main/java/com/nextcloud/client/di/InjectorNotFoundException.java b/app/src/main/java/com/nextcloud/client/di/InjectorNotFoundException.java index 398f4db..7c802f4 100644 --- a/app/src/main/java/com/nextcloud/client/di/InjectorNotFoundException.java +++ b/app/src/main/java/com/nextcloud/client/di/InjectorNotFoundException.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.di; diff --git a/app/src/main/java/com/nextcloud/client/di/ThemeModule.kt b/app/src/main/java/com/nextcloud/client/di/ThemeModule.kt index 758060b..99a32ad 100644 --- a/app/src/main/java/com/nextcloud/client/di/ThemeModule.kt +++ b/app/src/main/java/com/nextcloud/client/di/ThemeModule.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.di @@ -27,19 +27,14 @@ internal abstract class ThemeModule { @Provides @Singleton - fun themeColorUtils(): ThemeColorUtils { - return ThemeColorUtils() - } + fun themeColorUtils(): ThemeColorUtils = ThemeColorUtils() @Provides @Singleton - fun themeUtils(): ThemeUtils { - return ThemeUtils() - } + fun themeUtils(): ThemeUtils = ThemeUtils() @Provides - fun provideMaterialSchemes(materialSchemesProvider: MaterialSchemesProvider): MaterialSchemes { - return materialSchemesProvider.getMaterialSchemesForCurrentUser() - } + fun provideMaterialSchemes(materialSchemesProvider: MaterialSchemesProvider): MaterialSchemes = + materialSchemesProvider.getMaterialSchemesForCurrentUser() } } diff --git a/app/src/main/java/com/nextcloud/client/di/ViewModelFactory.kt b/app/src/main/java/com/nextcloud/client/di/ViewModelFactory.kt index 3b1b6b0..08041ae 100644 --- a/app/src/main/java/com/nextcloud/client/di/ViewModelFactory.kt +++ b/app/src/main/java/com/nextcloud/client/di/ViewModelFactory.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.di diff --git a/app/src/main/java/com/nextcloud/client/di/ViewModelKey.kt b/app/src/main/java/com/nextcloud/client/di/ViewModelKey.kt index 0d83151..baf2676 100644 --- a/app/src/main/java/com/nextcloud/client/di/ViewModelKey.kt +++ b/app/src/main/java/com/nextcloud/client/di/ViewModelKey.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.di diff --git a/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt b/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt index 5fd38ab..eb3e98a 100644 --- a/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt +++ b/app/src/main/java/com/nextcloud/client/di/ViewModelModule.kt @@ -1,8 +1,9 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2024 TSI-mc * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.di @@ -13,6 +14,7 @@ import com.nextcloud.client.etm.EtmViewModel import com.nextcloud.client.logger.ui.LogsViewModel import com.nextcloud.ui.fileactions.FileActionsViewModel import com.owncloud.android.ui.preview.pdf.PreviewPdfViewModel +import com.nextcloud.ui.trashbinFileActions.TrashbinFileActionsViewModel import com.owncloud.android.ui.unifiedsearch.UnifiedSearchViewModel import dagger.Binds import dagger.Module @@ -50,6 +52,11 @@ abstract class ViewModelModule { @ViewModelKey(DocumentScanViewModel::class) abstract fun documentScanViewModel(vm: DocumentScanViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(TrashbinFileActionsViewModel::class) + abstract fun trashbinFileActionsViewModel(vm: TrashbinFileActionsViewModel): ViewModel + @Binds abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory } diff --git a/app/src/main/java/com/nextcloud/client/di/package-info.java b/app/src/main/java/com/nextcloud/client/di/package-info.java index 2d6cb6d..e7e0175 100644 --- a/app/src/main/java/com/nextcloud/client/di/package-info.java +++ b/app/src/main/java/com/nextcloud/client/di/package-info.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ /** diff --git a/app/src/main/java/com/nextcloud/client/documentscan/AppScanOptionalFeature.kt b/app/src/main/java/com/nextcloud/client/documentscan/AppScanOptionalFeature.kt index e2b4505..d6996e1 100644 --- a/app/src/main/java/com/nextcloud/client/documentscan/AppScanOptionalFeature.kt +++ b/app/src/main/java/com/nextcloud/client/documentscan/AppScanOptionalFeature.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Álvaro Brey * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.documentscan @@ -21,9 +21,8 @@ abstract class AppScanOptionalFeature { */ @Suppress("unused") // used only in some variants object Stub : AppScanOptionalFeature() { - override fun getScanContract(): ActivityResultContract { + override fun getScanContract(): ActivityResultContract = throw UnsupportedOperationException("Document scan is not available") - } override val isAvailable = false } diff --git a/app/src/main/java/com/nextcloud/client/documentscan/DocumentPageListAdapter.kt b/app/src/main/java/com/nextcloud/client/documentscan/DocumentPageListAdapter.kt index 0d8e7de..17ff411 100644 --- a/app/src/main/java/com/nextcloud/client/documentscan/DocumentPageListAdapter.kt +++ b/app/src/main/java/com/nextcloud/client/documentscan/DocumentPageListAdapter.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.documentscan @@ -28,9 +28,7 @@ class DocumentPageListAdapter : holder.bind(currentList[position]) } - override fun getItemCount(): Int { - return currentList.size - } + override fun getItemCount(): Int = currentList.size class DocumentPageViewHolder(val binding: DocumentPageItemBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(imagePath: String) { @@ -39,10 +37,8 @@ class DocumentPageListAdapter : } private class DiffItemCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: String, newItem: String) = - oldItem == newItem + override fun areItemsTheSame(oldItem: String, newItem: String) = oldItem == newItem - override fun areContentsTheSame(oldItem: String, newItem: String) = - oldItem == newItem + override fun areContentsTheSame(oldItem: String, newItem: String) = oldItem == newItem } } diff --git a/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanActivity.kt b/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanActivity.kt index 349152f..daf9487 100644 --- a/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanActivity.kt +++ b/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanActivity.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.documentscan @@ -27,7 +27,9 @@ import com.owncloud.android.ui.activity.ToolbarActivity import com.owncloud.android.utils.theme.ViewThemeUtils import javax.inject.Inject -class DocumentScanActivity : ToolbarActivity(), Injectable { +class DocumentScanActivity : + ToolbarActivity(), + Injectable { @Inject lateinit var vmFactory: ViewModelFactory @@ -96,18 +98,16 @@ class DocumentScanActivity : ToolbarActivity(), Injectable { } } - override fun onMenuItemSelected(menuItem: MenuItem): Boolean { - return when (menuItem.itemId) { - R.id.action_save -> { - viewModel.onClickDone() - true - } - android.R.id.home -> { - onBackPressed() - true - } - else -> false + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.action_save -> { + viewModel.onClickDone() + true } + android.R.id.home -> { + onBackPressedDispatcher.onBackPressed() + true + } + else -> false } } ) diff --git a/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanViewModel.kt b/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanViewModel.kt index b74d7b3..e7cc433 100644 --- a/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/documentscan/DocumentScanViewModel.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.documentscan @@ -46,15 +46,11 @@ class DocumentScanViewModel @Inject constructor( get() = pageList.isEmpty() } - class NormalState( - pageList: List = emptyList(), - val shouldRequestScan: Boolean = false - ) : BaseState(pageList) + class NormalState(pageList: List = emptyList(), val shouldRequestScan: Boolean = false) : + BaseState(pageList) - class RequestExportState( - pageList: List = emptyList(), - val shouldRequestExportType: Boolean = true - ) : BaseState(pageList) + class RequestExportState(pageList: List = emptyList(), val shouldRequestExportType: Boolean = true) : + BaseState(pageList) object DoneState : UIState object CanceledState : UIState diff --git a/app/src/main/java/com/nextcloud/client/documentscan/GeneratePDFUseCase.kt b/app/src/main/java/com/nextcloud/client/documentscan/GeneratePDFUseCase.kt index b25d819..0f6cfa2 100644 --- a/app/src/main/java/com/nextcloud/client/documentscan/GeneratePDFUseCase.kt +++ b/app/src/main/java/com/nextcloud/client/documentscan/GeneratePDFUseCase.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Álvaro Brey * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.documentscan @@ -22,40 +22,30 @@ class GeneratePDFUseCase @Inject constructor(private val logger: Logger) { * @param imagePaths list of image paths * @return `true` if the PDF was generated successfully, `false` otherwise */ - fun execute(imagePaths: List, filePath: String): Boolean { - return if (imagePaths.isEmpty() || filePath.isBlank()) { - logger.w(TAG, "Invalid parameters: imagePaths: $imagePaths, filePath: $filePath") - false - } else { - val document = PdfDocument() - fillDocumentPages(document, imagePaths) - writePdfToFile(filePath, document) - } + fun execute(imagePaths: List, filePath: String): Boolean = if (imagePaths.isEmpty() || filePath.isBlank()) { + logger.w(TAG, "Invalid parameters: imagePaths: $imagePaths, filePath: $filePath") + false + } else { + val document = PdfDocument() + fillDocumentPages(document, imagePaths) + writePdfToFile(filePath, document) } /** * @return `true` if the PDF was generated successfully, `false` otherwise */ - private fun writePdfToFile( - filePath: String, - document: PdfDocument - ): Boolean { - return try { - val fileOutputStream = FileOutputStream(filePath) - document.writeTo(fileOutputStream) - fileOutputStream.close() - document.close() - true - } catch (ex: IOException) { - logger.e(TAG, "Error generating PDF", ex) - false - } + private fun writePdfToFile(filePath: String, document: PdfDocument): Boolean = try { + val fileOutputStream = FileOutputStream(filePath) + document.writeTo(fileOutputStream) + fileOutputStream.close() + document.close() + true + } catch (ex: IOException) { + logger.e(TAG, "Error generating PDF", ex) + false } - private fun fillDocumentPages( - document: PdfDocument, - imagePaths: List - ) { + private fun fillDocumentPages(document: PdfDocument, imagePaths: List) { imagePaths.forEach { path -> val bitmap = BitmapFactory.decodeFile(path) val pageInfo = PdfDocument.PageInfo.Builder(bitmap.width, bitmap.height, 1).create() diff --git a/app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt b/app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt index f56b1b8..c1115de 100644 --- a/app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt +++ b/app/src/main/java/com/nextcloud/client/documentscan/GeneratePdfFromImagesWork.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.documentscan @@ -112,7 +112,8 @@ class GeneratePdfFromImagesWork( user, arrayOf(pdfPath), arrayOf(uploadPath), - FileUploadWorker.LOCAL_BEHAVIOUR_DELETE, // MIME type will be detected from file name + // MIME type will be detected from file name + FileUploadWorker.LOCAL_BEHAVIOUR_DELETE, true, UploadFileOperation.CREATED_BY_USER, false, diff --git a/app/src/main/java/com/nextcloud/client/editimage/EditImageActivity.kt b/app/src/main/java/com/nextcloud/client/editimage/EditImageActivity.kt index b16e0f6..795f974 100644 --- a/app/src/main/java/com/nextcloud/client/editimage/EditImageActivity.kt +++ b/app/src/main/java/com/nextcloud/client/editimage/EditImageActivity.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 ZetaTom * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.editimage @@ -15,7 +15,6 @@ import android.view.Menu import android.view.MenuItem import android.view.View import androidx.appcompat.content.res.AppCompatResources -import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat @@ -59,9 +58,7 @@ class EditImageActivity : MimeType.HEIC ) - fun canBePreviewed(file: OCFile): Boolean { - return file.mimeType in supportedMimeTypes - } + fun canBePreviewed(file: OCFile): Boolean = file.mimeType in supportedMimeTypes } override fun onCreate(savedInstanceState: Bundle?) { @@ -82,7 +79,6 @@ class EditImageActivity : val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView) windowInsetsController.hide(WindowInsetsCompat.Type.statusBars()) - window.statusBarColor = ContextCompat.getColor(this, R.color.black) window.navigationBarColor = getColor(R.color.black) setupCropper() @@ -129,9 +125,7 @@ class EditImageActivity : } menu?.findItem(R.id.custom_menu_placeholder_item)?.apply { icon = saveIcon - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - contentDescription = getString(R.string.common_save) - } + contentDescription = getString(R.string.common_save) } return true } diff --git a/app/src/main/java/com/nextcloud/client/errorhandling/ExceptionHandler.kt b/app/src/main/java/com/nextcloud/client/errorhandling/ExceptionHandler.kt index 5da2509..f68f175 100644 --- a/app/src/main/java/com/nextcloud/client/errorhandling/ExceptionHandler.kt +++ b/app/src/main/java/com/nextcloud/client/errorhandling/ExceptionHandler.kt @@ -6,7 +6,7 @@ * SPDX-FileCopyrightText: 2019 Andy Scherzinger * SPDX-FileCopyrightText: 2019 Chris Narkiewicz * SPDX-FileCopyrightText: 2014 Luke Owncloud - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.nextcloud.client.errorhandling diff --git a/app/src/main/java/com/nextcloud/client/errorhandling/ShowErrorActivity.kt b/app/src/main/java/com/nextcloud/client/errorhandling/ShowErrorActivity.kt index c8d1be3..f9e4b12 100644 --- a/app/src/main/java/com/nextcloud/client/errorhandling/ShowErrorActivity.kt +++ b/app/src/main/java/com/nextcloud/client/errorhandling/ShowErrorActivity.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Andy Scherzinger - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.errorhandling @@ -17,7 +17,6 @@ import com.owncloud.android.R import com.owncloud.android.databinding.ActivityShowErrorBinding import com.owncloud.android.utils.ClipboardUtil import com.owncloud.android.utils.DisplayUtils -import java.net.URLEncoder class ShowErrorActivity : AppCompatActivity() { private lateinit var binding: ActivityShowErrorBinding @@ -51,10 +50,7 @@ class ShowErrorActivity : AppCompatActivity() { private fun reportIssue() { ClipboardUtil.copyToClipboard(this, binding.textViewError.text.toString(), false) - val issueLink = String.format( - getString(R.string.report_issue_link), - URLEncoder.encode(binding.textViewError.text.toString(), Charsets.UTF_8.name()) - ) + val issueLink = getString(R.string.report_issue_link) DisplayUtils.startLinkIntent(this, issueLink) Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_LONG).show() } @@ -64,14 +60,12 @@ class ShowErrorActivity : AppCompatActivity() { return super.onCreateOptionsMenu(menu) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.error_share -> { - onClickedShare() - true - } - else -> super.onOptionsItemSelected(item) + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + R.id.error_share -> { + onClickedShare() + true } + else -> super.onOptionsItemSelected(item) } private fun onClickedShare() { diff --git a/app/src/main/java/com/nextcloud/client/etm/EtmActivity.kt b/app/src/main/java/com/nextcloud/client/etm/EtmActivity.kt index 6052dc1..61fbe27 100644 --- a/app/src/main/java/com/nextcloud/client/etm/EtmActivity.kt +++ b/app/src/main/java/com/nextcloud/client/etm/EtmActivity.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.etm @@ -10,6 +10,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.MenuItem +import androidx.activity.OnBackPressedCallback import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import com.nextcloud.client.di.Injectable @@ -18,7 +19,9 @@ import com.owncloud.android.R import com.owncloud.android.ui.activity.ToolbarActivity import javax.inject.Inject -class EtmActivity : ToolbarActivity(), Injectable { +class EtmActivity : + ToolbarActivity(), + Injectable { companion object { @JvmStatic @@ -44,25 +47,30 @@ class EtmActivity : ToolbarActivity(), Injectable { onPageChanged(it) } ) + handleOnBackPressed() } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - android.R.id.home -> { - if (!vm.onBackPressed()) { - finish() - } - true + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + android.R.id.home -> { + if (!vm.onBackPressed()) { + finish() } - else -> super.onOptionsItemSelected(item) + true } + else -> super.onOptionsItemSelected(item) } - @Deprecated("Deprecated in Java") - override fun onBackPressed() { - if (!vm.onBackPressed()) { - super.onBackPressed() - } + private fun handleOnBackPressed() { + onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + val handledByVm = vm.onBackPressed() + + if (!handledByVm) { + isEnabled = false + onBackPressedDispatcher.onBackPressed() + } + } + }) } private fun onPageChanged(page: EtmMenuEntry?) { diff --git a/app/src/main/java/com/nextcloud/client/etm/EtmBaseFragment.kt b/app/src/main/java/com/nextcloud/client/etm/EtmBaseFragment.kt index a92bb5a..aac5488 100644 --- a/app/src/main/java/com/nextcloud/client/etm/EtmBaseFragment.kt +++ b/app/src/main/java/com/nextcloud/client/etm/EtmBaseFragment.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.etm diff --git a/app/src/main/java/com/nextcloud/client/etm/EtmMenuAdapter.kt b/app/src/main/java/com/nextcloud/client/etm/EtmMenuAdapter.kt index f8aea79..513ed74 100644 --- a/app/src/main/java/com/nextcloud/client/etm/EtmMenuAdapter.kt +++ b/app/src/main/java/com/nextcloud/client/etm/EtmMenuAdapter.kt @@ -2,10 +2,11 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.etm +import android.annotation.SuppressLint import android.content.Context import android.view.LayoutInflater import android.view.View @@ -15,13 +16,12 @@ import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.owncloud.android.R -class EtmMenuAdapter( - context: Context, - val onItemClicked: (Int) -> Unit -) : RecyclerView.Adapter() { +class EtmMenuAdapter(context: Context, val onItemClicked: (Int) -> Unit) : + RecyclerView.Adapter() { private val layoutInflater = LayoutInflater.from(context) var pages: List = listOf() + @SuppressLint("NotifyDataSetChanged") set(value) { field = value notifyDataSetChanged() @@ -49,7 +49,5 @@ class EtmMenuAdapter( holder.secondaryAction.setImageResource(0) } - override fun getItemCount(): Int { - return pages.size - } + override fun getItemCount(): Int = pages.size } diff --git a/app/src/main/java/com/nextcloud/client/etm/EtmMenuEntry.kt b/app/src/main/java/com/nextcloud/client/etm/EtmMenuEntry.kt index 09ad91d..6d4ff08 100644 --- a/app/src/main/java/com/nextcloud/client/etm/EtmMenuEntry.kt +++ b/app/src/main/java/com/nextcloud/client/etm/EtmMenuEntry.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.etm diff --git a/app/src/main/java/com/nextcloud/client/etm/EtmMenuFragment.kt b/app/src/main/java/com/nextcloud/client/etm/EtmMenuFragment.kt index c6bb397..ace0f99 100644 --- a/app/src/main/java/com/nextcloud/client/etm/EtmMenuFragment.kt +++ b/app/src/main/java/com/nextcloud/client/etm/EtmMenuFragment.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.etm diff --git a/app/src/main/java/com/nextcloud/client/etm/EtmViewModel.kt b/app/src/main/java/com/nextcloud/client/etm/EtmViewModel.kt index c40b30e..6007973 100644 --- a/app/src/main/java/com/nextcloud/client/etm/EtmViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/etm/EtmViewModel.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.etm @@ -76,7 +76,7 @@ class EtmViewModel @Inject constructor( pageClass = EtmPreferencesFragment::class ), EtmMenuEntry( - iconRes = R.drawable.ic_user, + iconRes = R.drawable.ic_user_outline, titleRes = R.string.etm_accounts, pageClass = EtmAccountsFragment::class ), diff --git a/app/src/main/java/com/nextcloud/client/etm/pages/EtmAccountsFragment.kt b/app/src/main/java/com/nextcloud/client/etm/pages/EtmAccountsFragment.kt index be6e1e2..b05e7c4 100644 --- a/app/src/main/java/com/nextcloud/client/etm/pages/EtmAccountsFragment.kt +++ b/app/src/main/java/com/nextcloud/client/etm/pages/EtmAccountsFragment.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.etm.pages @@ -20,7 +20,7 @@ import com.owncloud.android.databinding.FragmentEtmAccountsBinding class EtmAccountsFragment : EtmBaseFragment() { private var _binding: FragmentEtmAccountsBinding? = null - private val binding get() = _binding!! + val binding get() = _binding!! override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -50,14 +50,12 @@ class EtmAccountsFragment : EtmBaseFragment() { inflater.inflate(R.menu.fragment_etm_accounts, menu) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.etm_accounts_share -> { - onClickedShare() - true - } - else -> super.onOptionsItemSelected(item) + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + R.id.etm_accounts_share -> { + onClickedShare() + true } + else -> super.onOptionsItemSelected(item) } private fun onClickedShare() { diff --git a/app/src/main/java/com/nextcloud/client/etm/pages/EtmBackgroundJobsFragment.kt b/app/src/main/java/com/nextcloud/client/etm/pages/EtmBackgroundJobsFragment.kt index a335059..a86dc38 100644 --- a/app/src/main/java/com/nextcloud/client/etm/pages/EtmBackgroundJobsFragment.kt +++ b/app/src/main/java/com/nextcloud/client/etm/pages/EtmBackgroundJobsFragment.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.etm.pages @@ -16,6 +16,7 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.core.view.isVisible import androidx.lifecycle.Observer import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -30,7 +31,9 @@ import java.text.SimpleDateFormat import java.util.Locale import javax.inject.Inject -class EtmBackgroundJobsFragment : EtmBaseFragment(), Injectable { +class EtmBackgroundJobsFragment : + EtmBaseFragment(), + Injectable { @Inject lateinit var preferences: AppPreferences @@ -51,9 +54,9 @@ class EtmBackgroundJobsFragment : EtmBaseFragment(), Injectable { private val executionLogRow = view.findViewById(R.id.etm_background_execution_logs_row) val executionTimesRow = view.findViewById(R.id.etm_background_execution_times_row) - var progressEnabled: Boolean = progressRow.visibility == View.VISIBLE + var progressEnabled: Boolean = progressRow.isVisible get() { - return progressRow.visibility == View.VISIBLE + return progressRow.isVisible } set(value) { field = value @@ -64,9 +67,9 @@ class EtmBackgroundJobsFragment : EtmBaseFragment(), Injectable { } } - var logsEnabled: Boolean = executionLogRow.visibility == View.VISIBLE + var logsEnabled: Boolean = executionLogRow.isVisible get() { - return executionLogRow.visibility == View.VISIBLE + return executionLogRow.isVisible } set(value) { field = value @@ -80,6 +83,7 @@ class EtmBackgroundJobsFragment : EtmBaseFragment(), Injectable { private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:MM:ssZ", Locale.getDefault()) var backgroundJobs: List = emptyList() + @SuppressLint("NotifyDataSetChanged") set(value) { field = value notifyDataSetChanged() @@ -96,9 +100,7 @@ class EtmBackgroundJobsFragment : EtmBaseFragment(), Injectable { return viewHolder } - override fun getItemCount(): Int { - return backgroundJobs.size - } + override fun getItemCount(): Int = backgroundJobs.size @SuppressLint("SetTextI18n") override fun onBindViewHolder(vh: ViewHolder, position: Int) { @@ -164,40 +166,40 @@ class EtmBackgroundJobsFragment : EtmBaseFragment(), Injectable { return view } + @Deprecated("Deprecated in Java") override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) inflater.inflate(R.menu.fragment_etm_background_jobs, menu) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.etm_background_jobs_cancel -> { - vm.cancelAllJobs() - true - } - - R.id.etm_background_jobs_prune -> { - vm.pruneJobs() - true - } - - R.id.etm_background_jobs_start_test -> { - vm.startTestJob(periodic = false) - true - } - - R.id.etm_background_jobs_schedule_test -> { - vm.startTestJob(periodic = true) - true - } - - R.id.etm_background_jobs_cancel_test -> { - vm.cancelTestJob() - true - } - - else -> super.onOptionsItemSelected(item) + @Deprecated("Deprecated in Java") + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + R.id.etm_background_jobs_cancel -> { + vm.cancelAllJobs() + true } + + R.id.etm_background_jobs_prune -> { + vm.pruneJobs() + true + } + + R.id.etm_background_jobs_start_test -> { + vm.startTestJob(periodic = false) + true + } + + R.id.etm_background_jobs_schedule_test -> { + vm.startTestJob(periodic = true) + true + } + + R.id.etm_background_jobs_cancel_test -> { + vm.cancelTestJob() + true + } + + else -> super.onOptionsItemSelected(item) } private fun onBackgroundJobsUpdated(backgroundJobs: List) { diff --git a/app/src/main/java/com/nextcloud/client/etm/pages/EtmFileTransferFragment.kt b/app/src/main/java/com/nextcloud/client/etm/pages/EtmFileTransferFragment.kt index 07c0fde..7af132b 100644 --- a/app/src/main/java/com/nextcloud/client/etm/pages/EtmFileTransferFragment.kt +++ b/app/src/main/java/com/nextcloud/client/etm/pages/EtmFileTransferFragment.kt @@ -2,10 +2,11 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.etm.pages +import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.Menu @@ -15,6 +16,7 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.core.view.isVisible import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -26,6 +28,7 @@ import com.nextcloud.client.jobs.transfer.TransferManager import com.owncloud.android.R import com.owncloud.android.datamodel.OCFile import com.owncloud.android.db.OCUpload +import java.util.Locale class EtmFileTransferFragment : EtmBaseFragment() { @@ -45,9 +48,9 @@ class EtmFileTransferFragment : EtmBaseFragment() { val progress = view.findViewById(R.id.etm_transfer_progress) private val progressRow = view.findViewById(R.id.etm_transfer_progress_row) - var progressEnabled: Boolean = progressRow.visibility == View.VISIBLE + var progressEnabled: Boolean = progressRow.isVisible get() { - return progressRow.visibility == View.VISIBLE + return progressRow.isVisible } set(value) { field = value @@ -61,6 +64,7 @@ class EtmFileTransferFragment : EtmBaseFragment() { private var transfers = listOf() + @SuppressLint("NotifyDataSetChanged") fun setStatus(status: TransferManager.Status) { transfers = listOf(status.pending, status.running, status.completed).flatten().reversed() notifyDataSetChanged() @@ -71,9 +75,7 @@ class EtmFileTransferFragment : EtmBaseFragment() { return ViewHolder(view) } - override fun getItemCount(): Int { - return transfers.size - } + override fun getItemCount(): Int = transfers.size override fun onBindViewHolder(vh: ViewHolder, position: Int) { val transfer = transfers[position] @@ -96,7 +98,7 @@ class EtmFileTransferFragment : EtmBaseFragment() { vh.state.text = transfer.state.toString() if (transfer.progress >= 0) { vh.progressEnabled = true - vh.progress.text = transfer.progress.toString() + vh.progress.text = String.format(Locale.getDefault(), "%d", transfer.progress) } else { vh.progressEnabled = false } @@ -137,18 +139,16 @@ class EtmFileTransferFragment : EtmBaseFragment() { inflater.inflate(R.menu.fragment_etm_file_transfer, menu) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.etm_test_download -> { - scheduleTestDownload() - true - } - R.id.etm_test_upload -> { - scheduleTestUpload() - true - } - else -> super.onOptionsItemSelected(item) + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + R.id.etm_test_download -> { + scheduleTestDownload() + true } + R.id.etm_test_upload -> { + scheduleTestUpload() + true + } + else -> super.onOptionsItemSelected(item) } private fun scheduleTestDownload() { diff --git a/app/src/main/java/com/nextcloud/client/etm/pages/EtmMigrations.kt b/app/src/main/java/com/nextcloud/client/etm/pages/EtmMigrations.kt index 4fe2652..3fd58f5 100644 --- a/app/src/main/java/com/nextcloud/client/etm/pages/EtmMigrations.kt +++ b/app/src/main/java/com/nextcloud/client/etm/pages/EtmMigrations.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.etm.pages @@ -20,7 +20,7 @@ import java.util.Locale class EtmMigrations : EtmBaseFragment() { private var _binding: FragmentEtmMigrationsBinding? = null - private val binding get() = _binding!! + val binding get() = _binding!! override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -40,7 +40,7 @@ class EtmMigrations : EtmBaseFragment() { fun showStatus() { val builder = StringBuilder() - val status = vm.migrationsStatus.toString().toLowerCase(Locale.US) + val status = vm.migrationsStatus.toString().lowercase(Locale.US) builder.append("Migration status: $status\n") val lastMigratedVersion = if (vm.lastMigratedVersion >= 0) { vm.lastMigratedVersion.toString() @@ -65,14 +65,12 @@ class EtmMigrations : EtmBaseFragment() { inflater.inflate(R.menu.fragment_etm_migrations, menu) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.etm_migrations_delete -> { - onDeleteMigrationsClicked() - true - } - else -> super.onOptionsItemSelected(item) + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + R.id.etm_migrations_delete -> { + onDeleteMigrationsClicked() + true } + else -> super.onOptionsItemSelected(item) } private fun onDeleteMigrationsClicked() { diff --git a/app/src/main/java/com/nextcloud/client/etm/pages/EtmPreferencesFragment.kt b/app/src/main/java/com/nextcloud/client/etm/pages/EtmPreferencesFragment.kt index 7513902..e10b29f 100644 --- a/app/src/main/java/com/nextcloud/client/etm/pages/EtmPreferencesFragment.kt +++ b/app/src/main/java/com/nextcloud/client/etm/pages/EtmPreferencesFragment.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.etm.pages @@ -20,7 +20,7 @@ import com.owncloud.android.databinding.FragmentEtmPreferencesBinding class EtmPreferencesFragment : EtmBaseFragment() { private var _binding: FragmentEtmPreferencesBinding? = null - private val binding get() = _binding!! + val binding get() = _binding!! override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -45,14 +45,12 @@ class EtmPreferencesFragment : EtmBaseFragment() { inflater.inflate(R.menu.fragment_etm_preferences, menu) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.etm_preferences_share -> { - onClickedShare() - true - } - else -> super.onOptionsItemSelected(item) + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + R.id.etm_preferences_share -> { + onClickedShare() + true } + else -> super.onOptionsItemSelected(item) } private fun onClickedShare() { diff --git a/app/src/main/java/com/nextcloud/client/files/DeepLinkConstants.kt b/app/src/main/java/com/nextcloud/client/files/DeepLinkConstants.kt new file mode 100644 index 0000000..0ad9842 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/files/DeepLinkConstants.kt @@ -0,0 +1,31 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 TSI-mc + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.files + +import com.owncloud.android.R + +enum class DeepLinkConstants(val route: String, val navId: Int) { + OPEN_FILES("openFiles", R.id.nav_all_files), + OPEN_FAVORITES("openFavorites", R.id.nav_favorites), + OPEN_MEDIA("openMedia", R.id.nav_gallery), + OPEN_SHARED("openShared", R.id.nav_shared), + OPEN_OFFLINE("openOffline", R.id.nav_on_device), + OPEN_NOTIFICATIONS("openNotifications", -1), + OPEN_DELETED("openDeleted", R.id.nav_trashbin), + OPEN_SETTINGS("openSettings", R.id.nav_settings), + + // Special case, handled separately + OPEN_AUTO_UPLOAD("openAutoUpload", -1), + OPEN_EXTERNAL_URL("openUrl", -1), + ACTION_CREATE_NEW("createNew", -1), + ACTION_APP_UPDATE("checkAppUpdate", -1); + + companion object { + fun fromPath(path: String?): DeepLinkConstants? = entries.find { it.route == path } + } +} diff --git a/app/src/main/java/com/nextcloud/client/files/DeepLinkHandler.kt b/app/src/main/java/com/nextcloud/client/files/DeepLinkHandler.kt index c4707f9..a7cc811 100644 --- a/app/src/main/java/com/nextcloud/client/files/DeepLinkHandler.kt +++ b/app/src/main/java/com/nextcloud/client/files/DeepLinkHandler.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.files @@ -18,9 +18,7 @@ import com.nextcloud.client.account.UserAccountManager * [com.nextcloud.client.mixins.ActivityMixin] and handle UI callbacks as well */ @Suppress("ForbiddenComment") -class DeepLinkHandler( - private val userAccountManager: UserAccountManager -) { +class DeepLinkHandler(private val userAccountManager: UserAccountManager) { /** * Provide parsed link arguments and context information required @@ -30,9 +28,9 @@ class DeepLinkHandler( companion object { val DEEP_LINK_PATTERN = Regex("""(.*?)(/index\.php)?/f/([0-9]+)$""") - val BASE_URL_GROUP_INDEX = 1 - val INDEX_PATH_GROUP_INDEX = 2 - val FILE_ID_GROUP_INDEX = 3 + const val BASE_URL_GROUP_INDEX = 1 + const val INDEX_PATH_GROUP_INDEX = 2 + const val FILE_ID_GROUP_INDEX = 3 } /** diff --git a/app/src/main/java/com/nextcloud/client/files/Direction.kt b/app/src/main/java/com/nextcloud/client/files/Direction.kt index e2f0402..d84987a 100644 --- a/app/src/main/java/com/nextcloud/client/files/Direction.kt +++ b/app/src/main/java/com/nextcloud/client/files/Direction.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.files diff --git a/app/src/main/java/com/nextcloud/client/files/Registry.kt b/app/src/main/java/com/nextcloud/client/files/Registry.kt index 29bb6e0..9e75643 100644 --- a/app/src/main/java/com/nextcloud/client/files/Registry.kt +++ b/app/src/main/java/com/nextcloud/client/files/Registry.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.files @@ -144,7 +144,5 @@ internal class Registry( * @param id transfer id * @return transfer status if found, null otherwise */ - fun getTransfer(uuid: UUID): Transfer? { - return pendingQueue[uuid] ?: runningQueue[uuid] ?: completedQueue[uuid] - } + fun getTransfer(uuid: UUID): Transfer? = pendingQueue[uuid] ?: runningQueue[uuid] ?: completedQueue[uuid] } diff --git a/app/src/main/java/com/nextcloud/client/files/Request.kt b/app/src/main/java/com/nextcloud/client/files/Request.kt index 61b1071..cc72bce 100644 --- a/app/src/main/java/com/nextcloud/client/files/Request.kt +++ b/app/src/main/java/com/nextcloud/client/files/Request.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.files @@ -18,13 +18,8 @@ import com.owncloud.android.db.OCUpload import com.owncloud.android.files.services.NameCollisionPolicy import java.util.UUID -sealed class Request( - val user: User, - val file: OCFile, - val uuid: UUID, - val type: Direction, - val test: Boolean -) : Parcelable +sealed class Request(val user: User, val file: OCFile, val uuid: UUID, val type: Direction, val test: Boolean) : + Parcelable /** * Transfer request. This class should collect all information @@ -74,18 +69,12 @@ class DownloadRequest internal constructor( parcel.writeInt(if (test) 1 else 0) } - override fun describeContents(): Int { - return 0 - } + override fun describeContents(): Int = 0 companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): DownloadRequest { - return DownloadRequest(parcel) - } + override fun createFromParcel(parcel: Parcel): DownloadRequest = DownloadRequest(parcel) - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } + override fun newArray(size: Int): Array = arrayOfNulls(size) } } @@ -138,18 +127,12 @@ class UploadRequest internal constructor( parcel.writeInt(if (test) 1 else 0) } - override fun describeContents(): Int { - return 0 - } + override fun describeContents(): Int = 0 companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): UploadRequest { - return UploadRequest(parcel) - } + override fun createFromParcel(parcel: Parcel): UploadRequest = UploadRequest(parcel) - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } + override fun newArray(size: Int): Array = arrayOfNulls(size) } /** diff --git a/app/src/main/java/com/nextcloud/client/integrations/IntegrationsModule.kt b/app/src/main/java/com/nextcloud/client/integrations/IntegrationsModule.kt index 6a88e5d..9a77053 100644 --- a/app/src/main/java/com/nextcloud/client/integrations/IntegrationsModule.kt +++ b/app/src/main/java/com/nextcloud/client/integrations/IntegrationsModule.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.integrations @@ -16,7 +16,5 @@ import dagger.Provides @Module class IntegrationsModule { @Provides - fun deckApi(context: Context, packageManager: PackageManager): DeckApi { - return DeckApiImpl(context, packageManager) - } + fun deckApi(context: Context, packageManager: PackageManager): DeckApi = DeckApiImpl(context, packageManager) } diff --git a/app/src/main/java/com/nextcloud/client/integrations/deck/DeckApi.kt b/app/src/main/java/com/nextcloud/client/integrations/deck/DeckApi.kt index 1880ce8..b956e4c 100644 --- a/app/src/main/java/com/nextcloud/client/integrations/deck/DeckApi.kt +++ b/app/src/main/java/com/nextcloud/client/integrations/deck/DeckApi.kt @@ -25,8 +25,5 @@ interface DeckApi { * value otherwise * @see [Deck Server App](https://apps.nextcloud.com/apps/deck) */ - fun createForwardToDeckActionIntent( - notification: Notification, - user: User - ): Optional + fun createForwardToDeckActionIntent(notification: Notification, user: User): Optional } diff --git a/app/src/main/java/com/nextcloud/client/integrations/deck/DeckApiImpl.kt b/app/src/main/java/com/nextcloud/client/integrations/deck/DeckApiImpl.kt index 2b9404a..82b1394 100644 --- a/app/src/main/java/com/nextcloud/client/integrations/deck/DeckApiImpl.kt +++ b/app/src/main/java/com/nextcloud/client/integrations/deck/DeckApiImpl.kt @@ -38,18 +38,16 @@ class DeckApiImpl(private val context: Context, private val packageManager: Pack ) } - private fun putExtrasToIntent(intent: Intent, notification: Notification, user: User): Intent { - return intent - .putExtra(EXTRA_ACCOUNT, user.accountName) - .putExtra(EXTRA_LINK, notification.getLink()) - .putExtra(EXTRA_OBJECT_ID, notification.getObjectId()) - .putExtra(EXTRA_SUBJECT, notification.getSubject()) - .putExtra(EXTRA_SUBJECT_RICH, notification.getSubjectRich()) - .putExtra(EXTRA_MESSAGE, notification.getMessage()) - .putExtra(EXTRA_MESSAGE_RICH, notification.getMessageRich()) - .putExtra(EXTRA_USER, notification.getUser()) - .putExtra(EXTRA_NID, notification.getNotificationId()) - } + private fun putExtrasToIntent(intent: Intent, notification: Notification, user: User): Intent = intent + .putExtra(EXTRA_ACCOUNT, user.accountName) + .putExtra(EXTRA_LINK, notification.getLink()) + .putExtra(EXTRA_OBJECT_ID, notification.getObjectId()) + .putExtra(EXTRA_SUBJECT, notification.getSubject()) + .putExtra(EXTRA_SUBJECT_RICH, notification.getSubjectRich()) + .putExtra(EXTRA_MESSAGE, notification.getMessage()) + .putExtra(EXTRA_MESSAGE_RICH, notification.getMessageRich()) + .putExtra(EXTRA_USER, notification.getUser()) + .putExtra(EXTRA_NID, notification.getNotificationId()) companion object { const val APP_NAME = "deck" diff --git a/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt b/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt index 6210523..ebcb5f7 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt @@ -8,7 +8,7 @@ * Copyright (C) 2017 Nextcloud GmbH. * Copyright (C) 2020 Chris Narkiewicz * -* SPDX-License-Identifier: AGPL-3.0-or-later +* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index b008710..401fee1 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs @@ -17,12 +17,17 @@ import androidx.work.WorkerFactory import androidx.work.WorkerParameters import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.core.Clock -import com.nextcloud.client.device.DeviceInfo +import com.nextcloud.client.database.NextcloudDatabase import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.documentscan.GeneratePDFUseCase import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork import com.nextcloud.client.integrations.deck.DeckApi +import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker +import com.nextcloud.client.jobs.autoUpload.FileSystemRepository import com.nextcloud.client.jobs.download.FileDownloadWorker +import com.nextcloud.client.jobs.metadata.MetadataWorker +import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsWorker +import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.logger.Logger import com.nextcloud.client.network.ConnectivityService @@ -48,7 +53,6 @@ class BackgroundJobFactory @Inject constructor( private val clock: Clock, private val powerManagementService: PowerManagementService, private val backgroundJobManager: Provider, - private val deviceInfo: DeviceInfo, private val accountManager: UserAccountManager, private val resources: Resources, private val arbitraryDataProvider: ArbitraryDataProvider, @@ -60,7 +64,8 @@ class BackgroundJobFactory @Inject constructor( private val viewThemeUtils: Provider, private val localBroadcastManager: Provider, private val generatePdfUseCase: GeneratePDFUseCase, - private val syncedFolderProvider: SyncedFolderProvider + private val syncedFolderProvider: SyncedFolderProvider, + private val database: NextcloudDatabase ) : WorkerFactory() { @SuppressLint("NewApi") @@ -82,7 +87,7 @@ class BackgroundJobFactory @Inject constructor( when (workerClass) { ContactsBackupWork::class -> createContactsBackupWork(context, workerParameters) ContactsImportWork::class -> createContactsImportWork(context, workerParameters) - FilesSyncWork::class -> createFilesSyncWork(context, workerParameters) + AutoUploadWorker::class -> createFilesSyncWork(context, workerParameters) OfflineSyncWork::class -> createOfflineSyncWork(context, workerParameters) MediaFoldersDetectionWork::class -> createMediaFoldersDetectionWork(context, workerParameters) NotificationWork::class -> createNotificationWork(context, workerParameters) @@ -95,39 +100,43 @@ class BackgroundJobFactory @Inject constructor( GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters) HealthStatusWork::class -> createHealthStatusWork(context, workerParameters) TestJob::class -> createTestJob(context, workerParameters) + OfflineOperationsWorker::class -> createOfflineOperationsWorker(context, workerParameters) + InternalTwoWaySyncWork::class -> createInternalTwoWaySyncWork(context, workerParameters) + MetadataWorker::class -> createMetadataWorker(context, workerParameters) + FolderDownloadWorker::class -> createFolderDownloadWorker(context, workerParameters) else -> null // caller falls back to default factory } } } - private fun createFilesExportWork( - context: Context, - params: WorkerParameters - ): ListenableWorker { - return FilesExportWork( - context, + private fun createOfflineOperationsWorker(context: Context, params: WorkerParameters): ListenableWorker = + OfflineOperationsWorker( accountManager.user, - contentResolver, + context, + connectivityService, viewThemeUtils.get(), params ) - } - private fun createContentObserverJob( - context: Context, - workerParameters: WorkerParameters - ): ListenableWorker { - return ContentObserverWork( + private fun createFilesExportWork(context: Context, params: WorkerParameters): ListenableWorker = FilesExportWork( + context, + accountManager.user, + contentResolver, + viewThemeUtils.get(), + params + ) + + private fun createContentObserverJob(context: Context, workerParameters: WorkerParameters): ListenableWorker = + ContentObserverWork( context, workerParameters, SyncedFolderProvider(contentResolver, preferences, clock), powerManagementService, backgroundJobManager.get() ) - } - private fun createContactsBackupWork(context: Context, params: WorkerParameters): ContactsBackupWork { - return ContactsBackupWork( + private fun createContactsBackupWork(context: Context, params: WorkerParameters): ContactsBackupWork = + ContactsBackupWork( context, params, resources, @@ -135,63 +144,55 @@ class BackgroundJobFactory @Inject constructor( contentResolver, accountManager ) - } - private fun createContactsImportWork(context: Context, params: WorkerParameters): ContactsImportWork { - return ContactsImportWork( + private fun createContactsImportWork(context: Context, params: WorkerParameters): ContactsImportWork = + ContactsImportWork( context, params, logger, contentResolver ) - } - private fun createCalendarBackupWork(context: Context, params: WorkerParameters): CalendarBackupWork { - return CalendarBackupWork( + private fun createCalendarBackupWork(context: Context, params: WorkerParameters): CalendarBackupWork = + CalendarBackupWork( context, params, contentResolver, accountManager, preferences ) - } - private fun createCalendarImportWork(context: Context, params: WorkerParameters): CalendarImportWork { - return CalendarImportWork( + private fun createCalendarImportWork(context: Context, params: WorkerParameters): CalendarImportWork = + CalendarImportWork( context, params, logger, contentResolver ) - } - private fun createFilesSyncWork(context: Context, params: WorkerParameters): FilesSyncWork { - return FilesSyncWork( - context = context, - params = params, - contentResolver = contentResolver, - userAccountManager = accountManager, - uploadsStorageManager = uploadsStorageManager, - connectivityService = connectivityService, - powerManagementService = powerManagementService, - syncedFolderProvider = syncedFolderProvider, - backgroundJobManager = backgroundJobManager.get() - ) - } + private fun createFilesSyncWork(context: Context, params: WorkerParameters): AutoUploadWorker = AutoUploadWorker( + context = context, + params = params, + userAccountManager = accountManager, + uploadsStorageManager = uploadsStorageManager, + connectivityService = connectivityService, + powerManagementService = powerManagementService, + syncedFolderProvider = syncedFolderProvider, + backgroundJobManager = backgroundJobManager.get(), + repository = FileSystemRepository(dao = database.fileSystemDao()) + ) - private fun createOfflineSyncWork(context: Context, params: WorkerParameters): OfflineSyncWork { - return OfflineSyncWork( - context = context, - params = params, - contentResolver = contentResolver, - userAccountManager = accountManager, - connectivityService = connectivityService, - powerManagementService = powerManagementService - ) - } + private fun createOfflineSyncWork(context: Context, params: WorkerParameters): OfflineSyncWork = OfflineSyncWork( + context = context, + params = params, + contentResolver = contentResolver, + userAccountManager = accountManager, + connectivityService = connectivityService, + powerManagementService = powerManagementService + ) - private fun createMediaFoldersDetectionWork(context: Context, params: WorkerParameters): MediaFoldersDetectionWork { - return MediaFoldersDetectionWork( + private fun createMediaFoldersDetectionWork(context: Context, params: WorkerParameters): MediaFoldersDetectionWork = + MediaFoldersDetectionWork( context, params, resources, @@ -202,21 +203,18 @@ class BackgroundJobFactory @Inject constructor( viewThemeUtils.get(), syncedFolderProvider ) - } - private fun createNotificationWork(context: Context, params: WorkerParameters): NotificationWork { - return NotificationWork( - context, - params, - notificationManager, - accountManager, - deckApi, - viewThemeUtils.get() - ) - } + private fun createNotificationWork(context: Context, params: WorkerParameters): NotificationWork = NotificationWork( + context, + params, + notificationManager, + accountManager, + deckApi, + viewThemeUtils.get() + ) - private fun createAccountRemovalWork(context: Context, params: WorkerParameters): AccountRemovalWork { - return AccountRemovalWork( + private fun createAccountRemovalWork(context: Context, params: WorkerParameters): AccountRemovalWork = + AccountRemovalWork( context, params, uploadsStorageManager, @@ -227,10 +225,9 @@ class BackgroundJobFactory @Inject constructor( preferences, syncedFolderProvider ) - } - private fun createFilesUploadWorker(context: Context, params: WorkerParameters): FileUploadWorker { - return FileUploadWorker( + private fun createFilesUploadWorker(context: Context, params: WorkerParameters): FileUploadWorker = + FileUploadWorker( uploadsStorageManager, connectivityService, powerManagementService, @@ -242,20 +239,18 @@ class BackgroundJobFactory @Inject constructor( context, params ) - } - private fun createFilesDownloadWorker(context: Context, params: WorkerParameters): FileDownloadWorker { - return FileDownloadWorker( + private fun createFilesDownloadWorker(context: Context, params: WorkerParameters): FileDownloadWorker = + FileDownloadWorker( viewThemeUtils.get(), accountManager, localBroadcastManager.get(), context, params ) - } - private fun createPDFGenerateWork(context: Context, params: WorkerParameters): GeneratePdfFromImagesWork { - return GeneratePdfFromImagesWork( + private fun createPDFGenerateWork(context: Context, params: WorkerParameters): GeneratePdfFromImagesWork = + GeneratePdfFromImagesWork( appContext = context, generatePdfUseCase = generatePdfUseCase, viewThemeUtils = viewThemeUtils.get(), @@ -264,23 +259,42 @@ class BackgroundJobFactory @Inject constructor( logger = logger, params = params ) - } - private fun createHealthStatusWork(context: Context, params: WorkerParameters): HealthStatusWork { - return HealthStatusWork( + private fun createHealthStatusWork(context: Context, params: WorkerParameters): HealthStatusWork = HealthStatusWork( + context, + params, + accountManager, + arbitraryDataProvider, + backgroundJobManager.get() + ) + + private fun createTestJob(context: Context, params: WorkerParameters): TestJob = TestJob( + context, + params, + backgroundJobManager.get() + ) + + private fun createInternalTwoWaySyncWork(context: Context, params: WorkerParameters): InternalTwoWaySyncWork = + InternalTwoWaySyncWork( context, params, accountManager, - arbitraryDataProvider, - backgroundJobManager.get() + powerManagementService, + connectivityService, + preferences ) - } - private fun createTestJob(context: Context, params: WorkerParameters): TestJob { - return TestJob( + private fun createMetadataWorker(context: Context, params: WorkerParameters): MetadataWorker = MetadataWorker( + context, + params, + accountManager.user + ) + + private fun createFolderDownloadWorker(context: Context, params: WorkerParameters): FolderDownloadWorker = + FolderDownloadWorker( + accountManager, context, - params, - backgroundJobManager.get() + viewThemeUtils.get(), + params ) - } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt index 68a7a02..219de80 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs @@ -10,6 +10,7 @@ import androidx.lifecycle.LiveData import androidx.work.ListenableWorker import com.nextcloud.client.account.User import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.SyncedFolder import com.owncloud.android.operations.DownloadType /** @@ -95,7 +96,7 @@ interface BackgroundJobManager { * @param contactsAccountName Target contacts account name; null for local contacts * @param contactsAccountType Target contacts account type; null for local contacts * @param vCardFilePath Path to file containing all contact entries - * @param selectedContacts List of contact indices to import from [vCardFilePath] file + * @param selectedContactsFilePath File path of list of contact indices to import from [vCardFilePath] file * * @return Job info with current status; status is null if job does not exist */ @@ -103,7 +104,7 @@ interface BackgroundJobManager { contactsAccountName: String?, contactsAccountType: String?, vCardFilePath: String, - selectedContacts: IntArray + selectedContactsFilePath: String ): LiveData /** @@ -119,13 +120,16 @@ interface BackgroundJobManager { fun startImmediateFilesExportJob(files: Collection): LiveData - fun schedulePeriodicFilesSyncJob() + fun schedulePeriodicFilesSyncJob(syncedFolder: SyncedFolder) - fun startImmediateFilesSyncJob( + fun startAutoUploadImmediately( + syncedFolder: SyncedFolder, overridePowerSaving: Boolean = false, - changedFiles: Array = arrayOf() + contentUris: Array = arrayOf() ) + fun cancelTwoWaySyncJob() + fun scheduleOfflineSync() fun scheduleMediaFoldersDetectionJob() @@ -133,15 +137,13 @@ interface BackgroundJobManager { fun startNotificationJob(subject: String, signature: String) fun startAccountRemovalJob(accountName: String, remoteWipe: Boolean) - fun startFilesUploadJob(user: User) + fun startFilesUploadJob(user: User, uploadIds: LongArray, showSameFileAlreadyExistsNotification: Boolean) fun getFileUploads(user: User): LiveData> fun cancelFilesUploadJob(user: User) - fun isStartFileUploadJobScheduled(user: User): Boolean + fun isStartFileUploadJobScheduled(accountName: String): Boolean fun cancelFilesDownloadJob(user: User, fileId: Long) - fun isStartFileDownloadJobScheduled(user: User, fileId: Long): Boolean - @Suppress("LongParameterList") fun startFileDownloadJob( user: User, @@ -163,4 +165,12 @@ interface BackgroundJobManager { fun cancelAllJobs() fun schedulePeriodicHealthStatus() fun startHealthStatus() + fun bothFilesSyncJobsRunning(syncedFolderID: Long): Boolean + fun startOfflineOperations() + fun startPeriodicallyOfflineOperation() + fun scheduleInternal2WaySync(intervalMinutes: Long) + fun cancelAllFilesDownloadJobs() + fun startMetadataSyncJob(currentDirPath: String) + fun downloadFolder(folder: OCFile, accountName: String) + fun cancelFolderDownload() } diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt index d581604..d476a92 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -2,13 +2,14 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs import android.provider.MediaStore import androidx.lifecycle.LiveData import androidx.lifecycle.map +import androidx.work.BackoffPolicy import androidx.work.Constraints import androidx.work.Data import androidx.work.ExistingPeriodicWorkPolicy @@ -25,12 +26,22 @@ import com.nextcloud.client.account.User import com.nextcloud.client.core.Clock import com.nextcloud.client.di.Injectable import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork +import com.nextcloud.client.jobs.autoUpload.AutoUploadWorker import com.nextcloud.client.jobs.download.FileDownloadWorker +import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker +import com.nextcloud.client.jobs.metadata.MetadataWorker +import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsWorker +import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.preferences.AppPreferences +import com.nextcloud.utils.extensions.isWorkRunning import com.nextcloud.utils.extensions.isWorkScheduled import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.SyncedFolder import com.owncloud.android.operations.DownloadType +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import java.util.Date import java.util.UUID import java.util.concurrent.TimeUnit @@ -55,10 +66,10 @@ internal class BackgroundJobManagerImpl( private val workManager: WorkManager, private val clock: Clock, private val preferences: AppPreferences -) : BackgroundJobManager, Injectable { +) : BackgroundJobManager, + Injectable { companion object { - const val TAG_ALL = "*" // This tag allows us to retrieve list of all jobs run by Nextcloud client const val JOB_CONTENT_OBSERVER = "content_observer" const val JOB_PERIODIC_CONTACTS_BACKUP = "periodic_contacts_backup" @@ -79,9 +90,13 @@ internal class BackgroundJobManagerImpl( const val JOB_PDF_GENERATION = "pdf_generation" const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup" const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export" - + const val JOB_OFFLINE_OPERATIONS = "offline_operations" + const val JOB_PERIODIC_OFFLINE_OPERATIONS = "periodic_offline_operations" const val JOB_PERIODIC_HEALTH_STATUS = "periodic_health_status" const val JOB_IMMEDIATE_HEALTH_STATUS = "immediate_health_status" + const val JOB_DOWNLOAD_FOLDER = "download_folder" + const val JOB_METADATA_SYNC = "metadata_sync" + const val JOB_INTERNAL_TWO_WAY_SYNC = "internal_two_way_sync" const val JOB_TEST = "test_job" @@ -95,16 +110,16 @@ internal class BackgroundJobManagerImpl( const val NOT_SET_VALUE = "not set" const val PERIODIC_BACKUP_INTERVAL_MINUTES = 24 * 60L const val DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES = 15L + const val OFFLINE_OPERATIONS_PERIODIC_JOB_INTERVAL_MINUTES = 5L const val DEFAULT_IMMEDIATE_JOB_DELAY_SEC = 3L + const val DEFAULT_BACKOFF_CRITERIA_DELAY_SEC = 300L private const val KEEP_LOG_MILLIS = 1000 * 60 * 60 * 24 * 3L - fun formatNameTag(name: String, user: User? = null): String { - return if (user == null) { - "$TAG_PREFIX_NAME:$name" - } else { - "$TAG_PREFIX_NAME:$name ${user.accountName}" - } + fun formatNameTag(name: String, user: User? = null): String = if (user == null) { + "$TAG_PREFIX_NAME:$name" + } else { + "$TAG_PREFIX_NAME:$name ${user.accountName}" } fun formatUserTag(user: User): String = "$TAG_PREFIX_USER:${user.accountName}" @@ -121,36 +136,32 @@ internal class BackgroundJobManagerImpl( } } - fun parseTimestamp(timestamp: String): Date { - return try { - val ms = timestamp.toLong() - Date(ms) - } catch (ex: NumberFormatException) { - Date(0) - } + fun parseTimestamp(timestamp: String): Date = try { + val ms = timestamp.toLong() + Date(ms) + } catch (ex: NumberFormatException) { + Date(0) } /** * Convert platform [androidx.work.WorkInfo] object into application-specific [JobInfo] model. * Conversion extracts work metadata from tags. */ - fun fromWorkInfo(info: WorkInfo?): JobInfo? { - return if (info != null) { - val metadata = mutableMapOf() - info.tags.forEach { parseTag(it)?.let { metadata[it.first] = it.second } } - val timestamp = parseTimestamp(metadata.get(TAG_PREFIX_START_TIMESTAMP) ?: "0") - JobInfo( - id = info.id, - state = info.state.toString(), - name = metadata.get(TAG_PREFIX_NAME) ?: NOT_SET_VALUE, - user = metadata.get(TAG_PREFIX_USER) ?: NOT_SET_VALUE, - started = timestamp, - progress = info.progress.getInt("progress", -1), - workerClass = metadata.get(TAG_PREFIX_CLASS) ?: NOT_SET_VALUE - ) - } else { - null - } + fun fromWorkInfo(info: WorkInfo?): JobInfo? = if (info != null) { + val metadata = mutableMapOf() + info.tags.forEach { parseTag(it)?.let { metadata[it.first] = it.second } } + val timestamp = parseTimestamp(metadata.get(TAG_PREFIX_START_TIMESTAMP) ?: "0") + JobInfo( + id = info.id, + state = info.state.toString(), + name = metadata.get(TAG_PREFIX_NAME) ?: NOT_SET_VALUE, + user = metadata.get(TAG_PREFIX_USER) ?: NOT_SET_VALUE, + started = timestamp, + progress = info.progress.getInt("progress", -1), + workerClass = metadata.get(TAG_PREFIX_CLASS) ?: NOT_SET_VALUE + ) + } else { + null } fun deleteOldLogs(logEntries: MutableList): MutableList { @@ -168,6 +179,8 @@ internal class BackgroundJobManagerImpl( } } + private val defaultDispatcherScope = CoroutineScope(Dispatchers.Default) + override fun logStartOfWorker(workerName: String?) { val logs = deleteOldLogs(preferences.readLogEntry().toMutableList()) @@ -195,13 +208,15 @@ internal class BackgroundJobManagerImpl( private fun oneTimeRequestBuilder( jobClass: KClass, jobName: String, - user: User? = null + user: User? = null, + constraints: Constraints = Constraints.Builder().build() ): OneTimeWorkRequest.Builder { val builder = OneTimeWorkRequest.Builder(jobClass.java) .addTag(TAG_ALL) .addTag(formatNameTag(jobName, user)) .addTag(formatTimeTag(clock.currentTime)) .addTag(formatClassTag(jobClass)) + .setConstraints(constraints) user?.let { builder.addTag(formatUserTag(it)) } return builder } @@ -214,7 +229,8 @@ internal class BackgroundJobManagerImpl( jobName: String, intervalMins: Long = DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES, flexIntervalMins: Long = DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES, - user: User? = null + user: User? = null, + constraints: Constraints = Constraints.Builder().build() ): PeriodicWorkRequest.Builder { val builder = PeriodicWorkRequest.Builder( jobClass.java, @@ -227,6 +243,7 @@ internal class BackgroundJobManagerImpl( .addTag(formatNameTag(jobName, user)) .addTag(formatTimeTag(clock.currentTime)) .addTag(formatClassTag(jobClass)) + .setConstraints(constraints) user?.let { builder.addTag(formatUserTag(it)) } return builder } @@ -298,13 +315,13 @@ internal class BackgroundJobManagerImpl( contactsAccountName: String?, contactsAccountType: String?, vCardFilePath: String, - selectedContacts: IntArray + selectedContactsFilePath: String ): LiveData { val data = Data.Builder() .putString(ContactsImportWork.ACCOUNT_NAME, contactsAccountName) .putString(ContactsImportWork.ACCOUNT_TYPE, contactsAccountType) .putString(ContactsImportWork.VCARD_FILE_PATH, vCardFilePath) - .putIntArray(ContactsImportWork.SELECTED_CONTACTS_INDICES, selectedContacts) + .putString(ContactsImportWork.SELECTED_CONTACTS_FILE_PATH, selectedContactsFilePath) .build() val constraints = Constraints.Builder() @@ -403,32 +420,161 @@ internal class BackgroundJobManagerImpl( workManager.cancelJob(JOB_PERIODIC_CALENDAR_BACKUP, user) } - override fun schedulePeriodicFilesSyncJob() { - val request = periodicRequestBuilder( - jobClass = FilesSyncWork::class, - jobName = JOB_PERIODIC_FILES_SYNC, - intervalMins = DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES - ).build() - workManager.enqueueUniquePeriodicWork(JOB_PERIODIC_FILES_SYNC, ExistingPeriodicWorkPolicy.REPLACE, request) - } + override fun bothFilesSyncJobsRunning(syncedFolderID: Long): Boolean = + workManager.isWorkRunning(JOB_PERIODIC_FILES_SYNC + "_" + syncedFolderID) && + workManager.isWorkRunning(JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID) - override fun startImmediateFilesSyncJob( - overridePowerSaving: Boolean, - changedFiles: Array - ) { - val arguments = Data.Builder() - .putBoolean(FilesSyncWork.OVERRIDE_POWER_SAVING, overridePowerSaving) - .putStringArray(FilesSyncWork.CHANGED_FILES, changedFiles) + override fun startPeriodicallyOfflineOperation() { + val inputData = Data.Builder() + .putString(OfflineOperationsWorker.JOB_NAME, JOB_PERIODIC_OFFLINE_OPERATIONS) .build() - val request = oneTimeRequestBuilder( - jobClass = FilesSyncWork::class, - jobName = JOB_IMMEDIATE_FILES_SYNC + val request = periodicRequestBuilder( + jobClass = OfflineOperationsWorker::class, + jobName = JOB_PERIODIC_OFFLINE_OPERATIONS, + intervalMins = OFFLINE_OPERATIONS_PERIODIC_JOB_INTERVAL_MINUTES ) + .setInputData(inputData) + .build() + + workManager.enqueueUniquePeriodicWork( + JOB_PERIODIC_OFFLINE_OPERATIONS, + ExistingPeriodicWorkPolicy.UPDATE, + request + ) + } + + override fun startOfflineOperations() { + val inputData = Data.Builder() + .putString(OfflineOperationsWorker.JOB_NAME, JOB_OFFLINE_OPERATIONS) + .build() + + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + // Backoff criteria define how the system should retry the task if it fails. + // LINEAR means each retry will be delayed linearly (e.g., 10s, 20s, 30s...) + // DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES is used as the initial delay duration. + val backoffCriteriaPolicy = BackoffPolicy.LINEAR + val backoffCriteriaDelay = DEFAULT_BACKOFF_CRITERIA_DELAY_SEC + + val request = + oneTimeRequestBuilder(OfflineOperationsWorker::class, JOB_OFFLINE_OPERATIONS, constraints = constraints) + .setBackoffCriteria( + backoffCriteriaPolicy, + backoffCriteriaDelay, + TimeUnit.SECONDS + ) + .setInputData(inputData) + .build() + + workManager.enqueueUniqueWork( + JOB_OFFLINE_OPERATIONS, + ExistingWorkPolicy.KEEP, + request + ) + } + + override fun schedulePeriodicFilesSyncJob(syncedFolder: SyncedFolder) { + val syncedFolderID = syncedFolder.id + + val arguments = Data.Builder() + .putLong(AutoUploadWorker.SYNCED_FOLDER_ID, syncedFolderID) + .build() + + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresCharging(syncedFolder.isChargingOnly) + .build() + + val request = periodicRequestBuilder( + jobClass = AutoUploadWorker::class, + jobName = JOB_PERIODIC_FILES_SYNC + "_" + syncedFolderID, + intervalMins = DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES, + constraints = constraints + ) + .setBackoffCriteria( + BackoffPolicy.LINEAR, + DEFAULT_BACKOFF_CRITERIA_DELAY_SEC, + TimeUnit.SECONDS + ) .setInputData(arguments) .build() - workManager.enqueueUniqueWork(JOB_IMMEDIATE_FILES_SYNC, ExistingWorkPolicy.APPEND, request) + workManager.enqueueUniquePeriodicWork( + JOB_PERIODIC_FILES_SYNC + "_" + syncedFolderID, + ExistingPeriodicWorkPolicy.KEEP, + request + ) + } + + override fun startAutoUploadImmediately( + syncedFolder: SyncedFolder, + overridePowerSaving: Boolean, + contentUris: Array + ) { + val syncedFolderID = syncedFolder.id + + val arguments = Data.Builder() + .putBoolean(AutoUploadWorker.OVERRIDE_POWER_SAVING, overridePowerSaving) + .putStringArray(AutoUploadWorker.CONTENT_URIS, contentUris) + .putLong(AutoUploadWorker.SYNCED_FOLDER_ID, syncedFolderID) + .build() + + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresCharging(syncedFolder.isChargingOnly) + .build() + + val request = oneTimeRequestBuilder( + jobClass = AutoUploadWorker::class, + jobName = JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID + ) + .setInputData(arguments) + .setConstraints(constraints) + .setBackoffCriteria( + BackoffPolicy.LINEAR, + DEFAULT_BACKOFF_CRITERIA_DELAY_SEC, + TimeUnit.SECONDS + ) + .build() + + workManager.enqueueUniqueWork( + JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID, + ExistingWorkPolicy.APPEND, + request + ) + } + + override fun cancelTwoWaySyncJob() { + workManager.cancelJob(JOB_INTERNAL_TWO_WAY_SYNC) + } + + override fun cancelAllFilesDownloadJobs() { + workManager.cancelAllWorkByTag(formatClassTag(FileDownloadWorker::class)) + } + + override fun startMetadataSyncJob(currentDirPath: String) { + val inputData = Data.Builder() + .putString(MetadataWorker.FILE_PATH, currentDirPath) + .build() + + val constrains = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresBatteryNotLow(true) + .build() + + val request = oneTimeRequestBuilder(MetadataWorker::class, JOB_METADATA_SYNC) + .setConstraints(constrains) + .setInputData(inputData) + .build() + + workManager.enqueueUniqueWork( + JOB_METADATA_SYNC, + ExistingWorkPolicy.REPLACE, + request + ) } override fun scheduleOfflineSync() { @@ -491,34 +637,72 @@ internal class BackgroundJobManagerImpl( workManager.enqueue(request) } - private fun startFileUploadJobTag(user: User): String { - return JOB_FILES_UPLOAD + user.accountName + private fun startFileUploadJobTag(accountName: String): String = JOB_FILES_UPLOAD + accountName + + override fun isStartFileUploadJobScheduled(accountName: String): Boolean = + workManager.isWorkScheduled(startFileUploadJobTag(accountName)) + + /** + * This method supports initiating uploads for various scenarios, including: + * - New upload batches + * - Failed uploads + * - FilesSyncWork + * - ... + * + * @param user The user for whom the upload job is being created. + * @param uploadIds Array of upload IDs to be processed. These IDs originate from multiple sources + * and cannot be determined directly from the account name or a single function + * within the worker. + */ + override fun startFilesUploadJob(user: User, uploadIds: LongArray, showSameFileAlreadyExistsNotification: Boolean) { + defaultDispatcherScope.launch { + val batchSize = FileUploadHelper.MAX_FILE_COUNT + val batches = uploadIds.toList().chunked(batchSize) + val tag = startFileUploadJobTag(user.accountName) + + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val dataBuilder = Data.Builder() + .putBoolean( + FileUploadWorker.SHOW_SAME_FILE_ALREADY_EXISTS_NOTIFICATION, + showSameFileAlreadyExistsNotification + ) + .putString(FileUploadWorker.ACCOUNT, user.accountName) + .putInt(FileUploadWorker.TOTAL_UPLOAD_SIZE, uploadIds.size) + + val workRequests = batches.mapIndexed { index, batch -> + dataBuilder + .putLongArray(FileUploadWorker.UPLOAD_IDS, batch.toLongArray()) + .putInt(FileUploadWorker.CURRENT_BATCH_INDEX, index) + + oneTimeRequestBuilder(FileUploadWorker::class, JOB_FILES_UPLOAD, user) + .addTag(tag) + .setInputData(dataBuilder.build()) + .setConstraints(constraints) + .build() + } + + // Chain the work requests sequentially + if (workRequests.isNotEmpty()) { + var workChain = workManager.beginUniqueWork( + tag, + ExistingWorkPolicy.APPEND_OR_REPLACE, + workRequests.first() + ) + + workRequests.drop(1).forEach { request -> + workChain = workChain.then(request) + } + + workChain.enqueue() + } + } } - override fun isStartFileUploadJobScheduled(user: User): Boolean { - return workManager.isWorkScheduled(startFileUploadJobTag(user)) - } - - override fun startFilesUploadJob(user: User) { - val data = workDataOf(FileUploadWorker.ACCOUNT to user.accountName) - - val tag = startFileUploadJobTag(user) - - val request = oneTimeRequestBuilder(FileUploadWorker::class, JOB_FILES_UPLOAD, user) - .addTag(tag) - .setInputData(data) - .build() - - workManager.enqueueUniqueWork(tag, ExistingWorkPolicy.KEEP, request) - } - - private fun startFileDownloadJobTag(user: User, fileId: Long): String { - return JOB_FOLDER_DOWNLOAD + user.accountName + fileId - } - - override fun isStartFileDownloadJobScheduled(user: User, fileId: Long): Boolean { - return workManager.isWorkScheduled(startFileDownloadJobTag(user, fileId)) - } + private fun startFileDownloadJobTag(user: User, fileId: Long): String = + JOB_FOLDER_DOWNLOAD + user.accountName + fileId override fun startFileDownloadJob( user: User, @@ -546,7 +730,9 @@ internal class BackgroundJobManagerImpl( .setInputData(data) .build() - workManager.enqueueUniqueWork(tag, ExistingWorkPolicy.REPLACE, request) + // Since for each file new FileDownloadWorker going to be scheduled, + // better to use ExistingWorkPolicy.KEEP policy. + workManager.enqueueUniqueWork(tag, ExistingWorkPolicy.KEEP, request) } override fun getFileUploads(user: User): LiveData> { @@ -625,4 +811,40 @@ internal class BackgroundJobManagerImpl( request ) } + + override fun scheduleInternal2WaySync(intervalMinutes: Long) { + val request = periodicRequestBuilder( + jobClass = InternalTwoWaySyncWork::class, + jobName = JOB_INTERNAL_TWO_WAY_SYNC, + intervalMins = intervalMinutes + ) + .setInitialDelay(intervalMinutes, TimeUnit.MINUTES) + .build() + + workManager.enqueueUniquePeriodicWork(JOB_INTERNAL_TWO_WAY_SYNC, ExistingPeriodicWorkPolicy.UPDATE, request) + } + + override fun downloadFolder(folder: OCFile, accountName: String) { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresStorageNotLow(true) + .build() + + val data = Data.Builder() + .putLong(FolderDownloadWorker.FOLDER_ID, folder.fileId) + .putString(FolderDownloadWorker.ACCOUNT_NAME, accountName) + .build() + + val request = oneTimeRequestBuilder(FolderDownloadWorker::class, JOB_DOWNLOAD_FOLDER) + .addTag(JOB_DOWNLOAD_FOLDER) + .setInputData(data) + .setConstraints(constraints) + .build() + + workManager.enqueueUniqueWork(JOB_DOWNLOAD_FOLDER, ExistingWorkPolicy.APPEND_OR_REPLACE, request) + } + + override fun cancelFolderDownload() { + workManager.cancelAllWorkByTag(JOB_DOWNLOAD_FOLDER) + } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/CalendarBackupWork.kt b/app/src/main/java/com/nextcloud/client/jobs/CalendarBackupWork.kt index decf554..ffd2485 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/CalendarBackupWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/CalendarBackupWork.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2021 Tobias Kaminsky * SPDX-FileCopyrightText: 2021 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs diff --git a/app/src/main/java/com/nextcloud/client/jobs/CalendarImportWork.kt b/app/src/main/java/com/nextcloud/client/jobs/CalendarImportWork.kt index 5d4f112..03a4d42 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/CalendarImportWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/CalendarImportWork.kt @@ -1,10 +1,11 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2017 Tobias Kaminsky * SPDX-FileCopyrightText: 2017 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs @@ -13,6 +14,7 @@ import android.content.Context import androidx.work.Worker import androidx.work.WorkerParameters import com.nextcloud.client.logger.Logger +import com.owncloud.android.lib.common.utils.Log_OC import net.fortuna.ical4j.data.CalendarBuilder import third_parties.sufficientlysecure.AndroidCalendar import third_parties.sufficientlysecure.CalendarSource @@ -28,37 +30,56 @@ class CalendarImportWork( companion object { const val TAG = "CalendarImportWork" - const val SELECTED_CALENDARS = "selected_contacts_indices" } + @Suppress("TooGenericExceptionCaught") override fun doWork(): Result { - val calendarPaths = inputData.getStringArray(SELECTED_CALENDARS) ?: arrayOf() - val calendars = inputData.keyValueMap as Map + val calendars = inputData.keyValueMap as? Map<*, *> + if (calendars == null) { + logger.d(TAG, "CalendarImportWork cancelled due to null empty input data") + return Result.failure() + } val calendarBuilder = CalendarBuilder() - for ((path, selectedCalendar) in calendars) { - logger.d(TAG, "Import calendar from $path") + for ((path, selectedCalendarIndex) in calendars) { + try { + if (path !is String || selectedCalendarIndex !is Int) { + logger.d(TAG, "Skipping wrong input data types: $path - $selectedCalendarIndex") + continue + } - val file = File(path) - val calendarSource = CalendarSource( - file.toURI().toURL().toString(), - null, - null, - null, - appContext - ) + logger.d(TAG, "Import calendar from $path") - val calendars = AndroidCalendar.loadAll(contentResolver)[0] + val file = File(path) + val calendarSource = CalendarSource( + file.toURI().toURL().toString(), + null, + null, + null, + appContext + ) - ProcessVEvent( - appContext, - calendarBuilder.build(calendarSource.stream), - selectedCalendar, - true - ).run() + val calendarList = AndroidCalendar.loadAll(contentResolver) + if (selectedCalendarIndex >= calendarList.size) { + logger.d(TAG, "Skipping selectedCalendarIndex out of bound") + continue + } + + val selectedCalendar = calendarList[selectedCalendarIndex] + + ProcessVEvent( + appContext, + calendarBuilder.build(calendarSource.stream), + selectedCalendar, + true + ).run() + } catch (e: Exception) { + Log_OC.e(TAG, "skipping calendarIndex: $selectedCalendarIndex due to: $e") + } } + logger.d(TAG, "CalendarImportWork successfully completed") return Result.success() } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/ContactsBackupWork.kt b/app/src/main/java/com/nextcloud/client/jobs/ContactsBackupWork.kt index 0d7ef0a..4a01346 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/ContactsBackupWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/ContactsBackupWork.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs diff --git a/app/src/main/java/com/nextcloud/client/jobs/ContactsImportWork.kt b/app/src/main/java/com/nextcloud/client/jobs/ContactsImportWork.kt index 9803ccd..2b231e4 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/ContactsImportWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/ContactsImportWork.kt @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2017 Tobias Kaminsky * SPDX-FileCopyrightText: 2017 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs @@ -16,12 +16,16 @@ import android.provider.ContactsContract import androidx.work.Worker import androidx.work.WorkerParameters import com.nextcloud.client.logger.Logger +import com.nextcloud.utils.extensions.toIntArray +import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.ui.fragment.contactsbackup.BackupListFragment import com.owncloud.android.ui.fragment.contactsbackup.VCardComparator import ezvcard.Ezvcard import ezvcard.VCard +import org.apache.commons.io.FileUtils import third_parties.ezvcard_android.ContactOperations import java.io.BufferedInputStream +import java.io.File import java.io.FileInputStream import java.io.IOException import java.util.Collections @@ -39,15 +43,27 @@ class ContactsImportWork( const val ACCOUNT_TYPE = "account_type" const val ACCOUNT_NAME = "account_name" const val VCARD_FILE_PATH = "vcard_file_path" - const val SELECTED_CONTACTS_INDICES = "selected_contacts_indices" + const val SELECTED_CONTACTS_FILE_PATH = "selected_contacts_file_path" } - @Suppress("ComplexMethod", "NestedBlockDepth") // legacy code + @Suppress("ComplexMethod", "NestedBlockDepth", "LongMethod", "ReturnCount") // legacy code override fun doWork(): Result { val vCardFilePath = inputData.getString(VCARD_FILE_PATH) ?: "" val contactsAccountName = inputData.getString(ACCOUNT_NAME) val contactsAccountType = inputData.getString(ACCOUNT_TYPE) - val selectedContactsIndices = inputData.getIntArray(SELECTED_CONTACTS_INDICES) ?: IntArray(0) + val selectedContactsFilePath = inputData.getString(SELECTED_CONTACTS_FILE_PATH) + if (selectedContactsFilePath == null) { + Log_OC.d(TAG, "selectedContactsFilePath is null") + return Result.failure() + } + + val selectedContactsFile = File(selectedContactsFilePath) + if (!selectedContactsFile.exists()) { + Log_OC.d(TAG, "selectedContactsFile not exists") + return Result.failure() + } + + val selectedContactsIndices = readCheckedContractsFromFile(selectedContactsFile) val inputStream = BufferedInputStream(FileInputStream(vCardFilePath)) val vCards = ArrayList() @@ -79,16 +95,21 @@ class ContactsImportWork( cursor.moveToNext() } } + for (contactIndex in selectedContactsIndices) { - val vCard = vCards[contactIndex] - if (BackupListFragment.getDisplayName(vCard).isEmpty()) { - if (!ownContactMap.containsKey(vCard)) { - operations.insertContact(vCard) + try { + val vCard = vCards[contactIndex] + if (BackupListFragment.getDisplayName(vCard).isEmpty()) { + if (!ownContactMap.containsKey(vCard)) { + operations.insertContact(vCard) + } else { + operations.updateContact(vCard, ownContactMap[vCard]) + } } else { - operations.updateContact(vCard, ownContactMap[vCard]) + operations.insertContact(vCard) // Insert All the contacts without name } - } else { - operations.insertContact(vCard) // Insert All the contacts without name + } catch (t: Throwable) { + Log_OC.e(TAG, "skipping contactIndex: $contactIndex due to: $t") } } } catch (e: Exception) { @@ -103,9 +124,20 @@ class ContactsImportWork( logger.e(TAG, "Error closing vCard stream", e) } + Log_OC.d(TAG, "ContractsImportWork successfully completed") + selectedContactsFile.delete() return Result.success() } + @Suppress("TooGenericExceptionCaught") + fun readCheckedContractsFromFile(file: File): IntArray = try { + val fileData = FileUtils.readFileToByteArray(file) + fileData.toIntArray() + } catch (e: Exception) { + Log_OC.e(TAG, "Exception readCheckedContractsFromFile: $e") + intArrayOf() + } + private fun getContactFromCursor(cursor: Cursor): VCard? { val lookupKey = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.LOOKUP_KEY)) val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey) diff --git a/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt b/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt index 718e527..6d421d2 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/ContentObserverWork.kt @@ -1,17 +1,27 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs +import android.app.Notification import android.content.Context -import androidx.work.Worker +import androidx.core.app.NotificationCompat +import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.nextcloud.client.device.PowerManagementService +import com.nextcloud.utils.ForegroundServiceHelper +import com.owncloud.android.R +import com.owncloud.android.datamodel.ForegroundServiceType import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.notifications.NotificationUtils +import com.owncloud.android.utils.FilesSyncHelper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext /** * This work is triggered when OS detects change in media folders. @@ -20,45 +30,113 @@ import com.owncloud.android.lib.common.utils.Log_OC * * This job must not be started on API < 24. */ +@Suppress("TooGenericExceptionCaught") class ContentObserverWork( - appContext: Context, + private val context: Context, private val params: WorkerParameters, - private val syncerFolderProvider: SyncedFolderProvider, + private val syncedFolderProvider: SyncedFolderProvider, private val powerManagementService: PowerManagementService, private val backgroundJobManager: BackgroundJobManager -) : Worker(appContext, params) { +) : CoroutineWorker(context, params) { - override fun doWork(): Result { - backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class)) - - if (params.triggeredContentUris.size > 0) { - Log_OC.d(TAG, "File-sync Content Observer detected files change") - checkAndStartFileSyncJob() - backgroundJobManager.startMediaFoldersDetectionJob() - } - recheduleSelf() - - val result = Result.success() - backgroundJobManager.logEndOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class), result) - return result + companion object { + private const val TAG = "🔍" + "ContentObserverWork" + private const val CHANNEL_ID = NotificationUtils.NOTIFICATION_CHANNEL_CONTENT_OBSERVER + private const val NOTIFICATION_ID = 774 } - private fun recheduleSelf() { + override suspend fun doWork(): Result = withContext(Dispatchers.IO) { + val workerName = BackgroundJobManagerImpl.formatClassTag(this@ContentObserverWork::class) + backgroundJobManager.logStartOfWorker(workerName) + Log_OC.d(TAG, "started") + + try { + if (params.triggeredContentUris.isNotEmpty()) { + Log_OC.d(TAG, "📸 content observer detected file changes.") + + val notificationTitle = context.getString(R.string.content_observer_work_notification_title) + val notification = createNotification(notificationTitle) + updateForegroundInfo(notification) + checkAndTriggerAutoUpload() + + // prevent worker fail because of another worker + try { + backgroundJobManager.startMediaFoldersDetectionJob() + } catch (e: Exception) { + Log_OC.d(TAG, "⚠️ media folder detection job failed :$e") + } + } else { + Log_OC.d(TAG, "⚠️ triggeredContentUris is empty — nothing to sync.") + } + + rescheduleSelf() + + val result = Result.success() + backgroundJobManager.logEndOfWorker(workerName, result) + Log_OC.d(TAG, "finished") + result + } catch (e: Exception) { + Log_OC.e(TAG, "❌ Exception in ContentObserverWork: ${e.message}", e) + Result.retry() + } + } + + private suspend fun updateForegroundInfo(notification: Notification) { + val foregroundInfo = ForegroundServiceHelper.createWorkerForegroundInfo( + NOTIFICATION_ID, + notification, + ForegroundServiceType.DataSync + ) + setForeground(foregroundInfo) + } + + private fun createNotification(title: String): Notification = NotificationCompat.Builder(context, CHANNEL_ID) + .setContentTitle(title) + .setSmallIcon(R.drawable.ic_find_in_page) + .setOngoing(true) + .setSound(null) + .setVibrate(null) + .setOnlyAlertOnce(true) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setSilent(true) + .build() + + /** + * Re-schedules this observer to ensure continuous monitoring of media changes. + */ + private fun rescheduleSelf() { + Log_OC.d(TAG, "🔁 Rescheduling ContentObserverWork for continued observation.") backgroundJobManager.scheduleContentObserverJob() } - private fun checkAndStartFileSyncJob() { - val syncFolders = syncerFolderProvider.countEnabledSyncedFolders() > 0 - if (!powerManagementService.isPowerSavingEnabled && syncFolders) { - val changedFiles = mutableListOf() - for (uri in params.triggeredContentUris) { - changedFiles.add(uri.toString()) - } - backgroundJobManager.startImmediateFilesSyncJob(false, changedFiles.toTypedArray()) + private suspend fun checkAndTriggerAutoUpload() = withContext(Dispatchers.IO) { + if (powerManagementService.isPowerSavingEnabled) { + Log_OC.w(TAG, "⚡ Power saving mode active — skipping file sync.") + return@withContext + } + + val enabledFoldersCount = syncedFolderProvider.countEnabledSyncedFolders() + if (enabledFoldersCount <= 0) { + Log_OC.w(TAG, "🚫 No enabled synced folders found — skipping file sync.") + return@withContext + } + + val contentUris = params.triggeredContentUris.map { uri -> + // adds uri strings e.g. content://media/external/images/media/2281 + uri.toString() + }.toTypedArray() + Log_OC.d(TAG, "📄 Content uris detected") + + try { + FilesSyncHelper.startAutoUploadImmediatelyWithContentUris( + syncedFolderProvider, + backgroundJobManager, + false, + contentUris + ) + Log_OC.d(TAG, "✅ auto upload triggered successfully for ${contentUris.size} file(s).") + } catch (e: Exception) { + Log_OC.e(TAG, "❌ Failed to start auto upload for changed files: ${e.message}", e) } } - - companion object { - val TAG: String = ContentObserverWork::class.java.simpleName - } } 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 058500f..812be5c 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/FilesExportWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/FilesExportWork.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs @@ -57,6 +57,8 @@ class FilesExportWork( } private fun exportFiles(fileIDs: LongArray): Int { + val fileDownloadHelper = FileDownloadHelper.instance() + var successfulExports = 0 fileIDs .asSequence() @@ -76,7 +78,11 @@ class FilesExportWork( showErrorNotification(successfulExports) } } else { - downloadFile(ocFile) + fileDownloadHelper.downloadFile( + user, + ocFile, + downloadType = DownloadType.EXPORT + ) } successfulExports++ @@ -95,14 +101,6 @@ class FilesExportWork( ) } - private fun downloadFile(ocFile: OCFile) { - FileDownloadHelper.instance().downloadFile( - user, - ocFile, - downloadType = DownloadType.EXPORT - ) - } - private fun showErrorNotification(successfulExports: Int) { val message = if (successfulExports == 0) { appContext.resources.getQuantityString(R.plurals.export_failed, successfulExports, successfulExports) diff --git a/app/src/main/java/com/nextcloud/client/jobs/FilesSyncWork.kt b/app/src/main/java/com/nextcloud/client/jobs/FilesSyncWork.kt index 58b5728..b3c6cb2 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/FilesSyncWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/FilesSyncWork.kt @@ -1,21 +1,19 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2024 Jonas Mayer * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2017 Mario Danic * SPDX-FileCopyrightText: 2017 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs import android.content.ContentResolver import android.content.Context import android.content.res.Resources -import android.os.Build import android.text.TextUtils -import androidx.core.app.NotificationCompat import androidx.exifinterface.media.ExifInterface -import androidx.work.ForegroundInfo import androidx.work.Worker import androidx.work.WorkerParameters import com.nextcloud.client.account.UserAccountManager @@ -25,10 +23,8 @@ import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.network.ConnectivityService import com.nextcloud.client.preferences.SubFolderRule import com.owncloud.android.R -import com.owncloud.android.datamodel.ArbitraryDataProvider import com.owncloud.android.datamodel.ArbitraryDataProviderImpl import com.owncloud.android.datamodel.FilesystemDataProvider -import com.owncloud.android.datamodel.ForegroundServiceType import com.owncloud.android.datamodel.MediaFolderType import com.owncloud.android.datamodel.SyncedFolder import com.owncloud.android.datamodel.SyncedFolderProvider @@ -36,7 +32,6 @@ import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.ui.activity.SettingsActivity -import com.owncloud.android.ui.notifications.NotificationUtils import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.FilesSyncHelper import com.owncloud.android.utils.MimeType @@ -64,106 +59,172 @@ class FilesSyncWork( const val TAG = "FilesSyncJob" const val OVERRIDE_POWER_SAVING = "overridePowerSaving" const val CHANGED_FILES = "changedFiles" - const val FOREGROUND_SERVICE_ID = 414 + const val SYNCED_FOLDER_ID = "syncedFolderId" } - @Suppress("MagicNumber") - private fun updateForegroundWorker(progressPercent: Int, useForegroundWorker: Boolean) { - if (!useForegroundWorker) { - return - } + private lateinit var syncedFolder: SyncedFolder - // update throughout worker execution to give use feedback how far worker is - val notification = NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_FILE_SYNC) - .setTicker(context.getString(R.string.autoupload_worker_foreground_info)) - .setContentText(context.getString(R.string.autoupload_worker_foreground_info)) - .setSmallIcon(R.drawable.notification_icon) - .setContentTitle(context.getString(R.string.autoupload_worker_foreground_info)) - .setOngoing(true) - .setProgress(100, progressPercent, false) - .build() - val foregroundInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - ForegroundInfo(FOREGROUND_SERVICE_ID, notification, ForegroundServiceType.DataSync.getId()) - } else { - ForegroundInfo(FOREGROUND_SERVICE_ID, notification) - } - - setForegroundAsync(foregroundInfo) - } - - @Suppress("MagicNumber") + @Suppress("MagicNumber", "ReturnCount") override fun doWork(): Result { - backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class)) - Log_OC.d(TAG, "File-sync worker started") + val syncFolderId = inputData.getLong(SYNCED_FOLDER_ID, -1) + val changedFiles = inputData.getStringArray(CHANGED_FILES) - val overridePowerSaving = inputData.getBoolean(OVERRIDE_POWER_SAVING, false) - // If we are in power save mode, better to postpone upload - if (powerManagementService.isPowerSavingEnabled && !overridePowerSaving) { - val result = Result.success() - backgroundJobManager.logEndOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class), result) - return result - } + backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class) + "_" + syncFolderId) + Log_OC.d(TAG, "AutoUpload started folder ID: $syncFolderId") + + // Create all the providers we'll need val resources = context.resources val lightVersion = resources.getBoolean(R.bool.syncedFolder_light) - FilesSyncHelper.restartJobsIfNeeded( + val filesystemDataProvider = FilesystemDataProvider(contentResolver) + val currentLocale = resources.configuration.locale + val dateFormat = SimpleDateFormat("yyyy:MM:dd HH:mm:ss", currentLocale) + dateFormat.timeZone = TimeZone.getTimeZone(TimeZone.getDefault().id) + + if (!setSyncedFolder(syncFolderId)) { + Log_OC.w(TAG, "AutoUpload skipped since syncedFolder ($syncFolderId) is not enabled!") + return logEndOfWorker(syncFolderId) + } + + // Always first try to schedule uploads to make sure files are uploaded even if worker was killed to early + uploadFilesFromFolder( + context, + resources, + lightVersion, + filesystemDataProvider, + currentLocale, + dateFormat, + syncedFolder + ) + + if (canExitEarly(changedFiles, syncFolderId)) { + Log_OC.w(TAG, "AutoUpload skipped canExit conditions are met") + return logEndOfWorker(syncFolderId) + } + + val user = userAccountManager.getUser(syncedFolder.account) + if (user.isPresent) { + var uploadIds = uploadsStorageManager.getCurrentUploadIds(user.get().accountName) + backgroundJobManager.startFilesUploadJob(user.get(), uploadIds, false) + } + + // Get changed files from ContentObserverWork (only images and videos) or by scanning filesystem + Log_OC.d( + TAG, + "AutoUpload (${syncedFolder.remotePath}) changed files from observer: " + + changedFiles.contentToString() + ) + collectChangedFiles(changedFiles) + Log_OC.d(TAG, "AutoUpload (${syncedFolder.remotePath}) finished checking files.") + + uploadFilesFromFolder( + context, + resources, + lightVersion, + filesystemDataProvider, + currentLocale, + dateFormat, + syncedFolder + ) + + FilesSyncHelper.restartUploadsIfNeeded( uploadsStorageManager, userAccountManager, connectivityService, powerManagementService ) - // Get changed files from ContentObserverWork (only images and videos) or by scanning filesystem - val changedFiles = inputData.getStringArray(CHANGED_FILES) - Log_OC.d(TAG, "File-sync worker changed files from observer: " + changedFiles.contentToString()) - collectChangedFiles(changedFiles) - Log_OC.d(TAG, "File-sync worker finished checking files.") + return logEndOfWorker(syncFolderId) + } - // Create all the providers we'll need - val filesystemDataProvider = FilesystemDataProvider(contentResolver) - val currentLocale = resources.configuration.locale - val dateFormat = SimpleDateFormat("yyyy:MM:dd HH:mm:ss", currentLocale) - dateFormat.timeZone = TimeZone.getTimeZone(TimeZone.getDefault().id) - - // start upload of changed / new files - val syncedFolders = syncedFolderProvider.syncedFolders - for ((index, syncedFolder) in syncedFolders.withIndex()) { - updateForegroundWorker( - (50 + (index.toDouble() / syncedFolders.size.toDouble()) * 50).toInt(), - changedFiles.isNullOrEmpty() - ) - if (syncedFolder.isEnabled) { - syncFolder( - context, - resources, - lightVersion, - filesystemDataProvider, - currentLocale, - dateFormat, - syncedFolder - ) - } - } - Log_OC.d(TAG, "File-sync worker finished") + private fun logEndOfWorker(syncFolderId: Long): Result { + Log_OC.d(TAG, "AutoUpload worker (${syncedFolder.remotePath}) finished") val result = Result.success() - backgroundJobManager.logEndOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class), result) + backgroundJobManager.logEndOfWorker( + BackgroundJobManagerImpl.formatClassTag(this::class) + + "_" + syncFolderId, + result + ) return result } + private fun setSyncedFolder(syncedFolderID: Long): Boolean { + val syncedFolderTmp = syncedFolderProvider.getSyncedFolderByID(syncedFolderID) + if (syncedFolderTmp == null || !syncedFolderTmp.isEnabled) { + return false + } + syncedFolder = syncedFolderTmp + return true + } + + @Suppress("ReturnCount") + private fun canExitEarly(changedFiles: Array?, syncedFolderID: Long): Boolean { + // If we are in power save mode better to postpone scan and upload + val overridePowerSaving = inputData.getBoolean(OVERRIDE_POWER_SAVING, false) + if ((powerManagementService.isPowerSavingEnabled && !overridePowerSaving)) { + Log_OC.w(TAG, "AutoUpload skipped powerSaving is enabled!") + return true + } + + if (syncedFolderID < 0) { + Log_OC.w(TAG, "AutoUpload skipped no valid syncedFolderID provided") + return true + } + + // or sync worker already running + if (backgroundJobManager.bothFilesSyncJobsRunning(syncedFolderID)) { + Log_OC.w(TAG, "AutoUpload skipped another worker instance is running for $syncedFolderID") + return true + } + + val calculatedScanInterval = + FilesSyncHelper.calculateScanInterval(syncedFolder, connectivityService, powerManagementService) + val totalScanInterval = (syncedFolder.lastScanTimestampMs + calculatedScanInterval) + val currentTime = System.currentTimeMillis() + val passedScanInterval = totalScanInterval <= currentTime + + Log_OC.d(TAG, "AutoUpload lastScanTimestampMs: " + syncedFolder.lastScanTimestampMs) + Log_OC.d(TAG, "AutoUpload calculatedScanInterval: $calculatedScanInterval") + Log_OC.d(TAG, "AutoUpload totalScanInterval: $totalScanInterval") + Log_OC.d(TAG, "AutoUpload currentTime: $currentTime") + Log_OC.d(TAG, "AutoUpload passedScanInterval: $passedScanInterval") + + if (!passedScanInterval && changedFiles.isNullOrEmpty() && !overridePowerSaving) { + Log_OC.w( + TAG, + "AutoUpload skipped since started before scan interval and nothing todo: " + syncedFolder.localPath + ) + return true + } + + if (syncedFolder.isChargingOnly && + !powerManagementService.battery.isCharging && + !powerManagementService.battery.isFull + ) { + Log_OC.w( + TAG, + "AutoUpload skipped since phone is not charging: " + syncedFolder.localPath + ) + return true + } + + return false + } + @Suppress("MagicNumber") private fun collectChangedFiles(changedFiles: Array?) { if (!changedFiles.isNullOrEmpty()) { - FilesSyncHelper.insertChangedEntries(syncedFolderProvider, changedFiles) + FilesSyncHelper.insertChangedEntries(syncedFolder, changedFiles) } else { - // Check every file in every synced folder for changes and update - // filesystemDataProvider database (potentially needs a long time so use foreground worker) - updateForegroundWorker(5, true) - FilesSyncHelper.insertAllDBEntries(syncedFolderProvider) - updateForegroundWorker(50, true) + // Check every file in synced folder for changes and update + // filesystemDataProvider database (potentially needs a long time) + FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder) } + syncedFolder.lastScanTimestampMs = System.currentTimeMillis() + syncedFolderProvider.updateSyncFolder(syncedFolder) } @Suppress("LongMethod") // legacy code - private fun syncFolder( + private fun uploadFilesFromFolder( context: Context, resources: Resources, lightVersion: Boolean, @@ -175,66 +236,90 @@ class FilesSyncWork( val uploadAction: Int? val needsCharging: Boolean val needsWifi: Boolean - var file: File val accountName = syncedFolder.account + val optionalUser = userAccountManager.getUser(accountName) if (!optionalUser.isPresent) { + Log_OC.w(TAG, "AutoUpload:uploadFilesFromFolder skipped user not present") return } + val user = optionalUser.get() - val arbitraryDataProvider: ArbitraryDataProvider? = if (lightVersion) { + val arbitraryDataProvider = if (lightVersion) { ArbitraryDataProviderImpl(context) } else { null } + + // Ensure only new files are processed for upload. + // Files that have been previously uploaded cannot be re-uploaded, + // even if they have been deleted or moved from the target folder, + // as they are already marked as uploaded in the database. val paths = filesystemDataProvider.getFilesForUpload( syncedFolder.localPath, syncedFolder.id.toString() ) - - if (paths.size == 0) { + if (paths.isEmpty()) { + Log_OC.w(TAG, "AutoUpload:uploadFilesFromFolder skipped paths is empty") return } val pathsAndMimes = paths.map { path -> - file = File(path) + val file = File(path) val localPath = file.absolutePath + val remotePath = getRemotePath(file, syncedFolder, sFormatter, lightVersion, resources, currentLocale) + val mimeType = MimeTypeUtil.getBestMimeTypeByFilename(localPath) + + Log_OC.d(TAG, "AutoUpload:pathsAndMimes file.path: ${file.path}") + Log_OC.d(TAG, "AutoUpload:pathsAndMimes localPath: $localPath") + Log_OC.d(TAG, "AutoUpload:pathsAndMimes remotePath: $remotePath") + Log_OC.d(TAG, "AutoUpload:pathsAndMimes mimeType: $mimeType") + Triple( localPath, - getRemotePath(file, syncedFolder, sFormatter, lightVersion, resources, currentLocale), - MimeTypeUtil.getBestMimeTypeByFilename(localPath) + remotePath, + mimeType ) } + val localPaths = pathsAndMimes.map { it.first }.toTypedArray() val remotePaths = pathsAndMimes.map { it.second }.toTypedArray() if (lightVersion) { + Log_OC.d(TAG, "AutoUpload:uploadFilesFromFolder light version is used") + needsCharging = resources.getBoolean(R.bool.syncedFolder_light_on_charging) - needsWifi = arbitraryDataProvider!!.getBooleanValue( + needsWifi = arbitraryDataProvider?.getBooleanValue( accountName, SettingsActivity.SYNCED_FOLDER_LIGHT_UPLOAD_ON_WIFI - ) + ) ?: true + val uploadActionString = resources.getString(R.string.syncedFolder_light_upload_behaviour) uploadAction = getUploadAction(uploadActionString) + Log_OC.d(TAG, "AutoUpload upload action is: $uploadAction") } else { + Log_OC.d(TAG, "AutoUpload:uploadFilesFromFolder not light version is used") + needsCharging = syncedFolder.isChargingOnly needsWifi = syncedFolder.isWifiOnly uploadAction = syncedFolder.uploadAction } + FileUploadHelper.instance().uploadNewFiles( user, localPaths, remotePaths, - uploadAction!!, - true, // create parent folder if not existent + uploadAction, + // create parent folder if not existent + true, UploadFileOperation.CREATED_AS_INSTANT_PICTURE, needsWifi, needsCharging, - syncedFolder.nameCollisionPolicy + syncedFolder.nameCollisionPolicy, + false ) for (path in paths) { - // TODO batch update filesystemDataProvider.updateFilesystemFileAsSentForUpload( path, syncedFolder.id.toString() @@ -255,10 +340,14 @@ class FilesSyncWork( val useSubfolders: Boolean val subFolderRule: SubFolderRule if (lightVersion) { + Log_OC.d(TAG, "AutoUpload:getRemotePath light version is used") + useSubfolders = resources.getBoolean(R.bool.syncedFolder_light_use_subfolders) remoteFolder = resources.getString(R.string.syncedFolder_remote_folder) subFolderRule = SubFolderRule.YEAR_MONTH } else { + Log_OC.d(TAG, "AutoUpload:getRemotePath not light version is used") + useSubfolders = syncedFolder.isSubfolderByDate remoteFolder = syncedFolder.remotePath subFolderRule = syncedFolder.subfolderRule @@ -286,6 +375,8 @@ class FilesSyncWork( ): Long { var lastModificationTime = file.lastModified() if (MediaFolderType.IMAGE == syncedFolder.type && hasExif(file)) { + Log_OC.d(TAG, "AutoUpload:calculateLastModificationTime exif found") + @Suppress("TooGenericExceptionCaught") // legacy code try { val exifInterface = ExifInterface(file.absolutePath) @@ -294,6 +385,9 @@ class FilesSyncWork( val pos = ParsePosition(0) val dateTime = formatter.parse(exifDate, pos) lastModificationTime = dateTime.time + Log_OC.w(TAG, "AutoUpload:calculateLastModificationTime calculatedTime is: $lastModificationTime") + } else { + Log_OC.w(TAG, "AutoUpload:calculateLastModificationTime exifDate is empty") } } catch (e: Exception) { Log_OC.d(TAG, "Failed to get the proper time " + e.localizedMessage) @@ -302,12 +396,10 @@ class FilesSyncWork( return lastModificationTime } - private fun getUploadAction(action: String): Int? { - return when (action) { - "LOCAL_BEHAVIOUR_FORGET" -> FileUploadWorker.LOCAL_BEHAVIOUR_FORGET - "LOCAL_BEHAVIOUR_MOVE" -> FileUploadWorker.LOCAL_BEHAVIOUR_MOVE - "LOCAL_BEHAVIOUR_DELETE" -> FileUploadWorker.LOCAL_BEHAVIOUR_DELETE - else -> FileUploadWorker.LOCAL_BEHAVIOUR_FORGET - } + private fun getUploadAction(action: String): Int = when (action) { + "LOCAL_BEHAVIOUR_FORGET" -> FileUploadWorker.LOCAL_BEHAVIOUR_FORGET + "LOCAL_BEHAVIOUR_MOVE" -> FileUploadWorker.LOCAL_BEHAVIOUR_MOVE + "LOCAL_BEHAVIOUR_DELETE" -> FileUploadWorker.LOCAL_BEHAVIOUR_DELETE + else -> FileUploadWorker.LOCAL_BEHAVIOUR_FORGET } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/HealthStatusWork.kt b/app/src/main/java/com/nextcloud/client/jobs/HealthStatusWork.kt index c658839..2e0fadb 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/HealthStatusWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/HealthStatusWork.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs diff --git a/app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt b/app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt new file mode 100644 index 0000000..1d1c89f --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/InternalTwoWaySyncWork.kt @@ -0,0 +1,136 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Tobias Kaminsky + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.device.PowerManagementService +import com.nextcloud.client.network.ConnectivityService +import com.nextcloud.client.preferences.AppPreferences +import com.owncloud.android.MainApp +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.SynchronizeFolderOperation +import com.owncloud.android.utils.FileStorageUtils +import java.io.File + +@Suppress("Detekt.NestedBlockDepth", "ReturnCount", "LongParameterList") +class InternalTwoWaySyncWork( + private val context: Context, + params: WorkerParameters, + private val userAccountManager: UserAccountManager, + private val powerManagementService: PowerManagementService, + private val connectivityService: ConnectivityService, + private val appPreferences: AppPreferences +) : Worker(context, params) { + private var shouldRun = true + private var operation: SynchronizeFolderOperation? = null + + override fun doWork(): Result { + Log_OC.d(TAG, "Worker started!") + + var result = true + + @Suppress("ComplexCondition") + if (!appPreferences.isTwoWaySyncEnabled || + powerManagementService.isPowerSavingEnabled || + !connectivityService.isConnected || + connectivityService.isInternetWalled || + !connectivityService.connectivity.isWifi + ) { + Log_OC.d(TAG, "Not starting due to constraints!") + return Result.success() + } + + val users = userAccountManager.allUsers + + for (user in users) { + val fileDataStorageManager = FileDataStorageManager(user, context.contentResolver) + val folders = fileDataStorageManager.getInternalTwoWaySyncFolders(user) + + for (folder in folders) { + if (!shouldRun) { + Log_OC.d(TAG, "Worker was stopped!") + return Result.failure() + } + + checkFreeSpace(folder)?.let { checkFreeSpaceResult -> + return checkFreeSpaceResult + } + + Log_OC.d(TAG, "Folder ${folder.remotePath}: started!") + operation = SynchronizeFolderOperation(context, folder.remotePath, user, fileDataStorageManager, true) + val operationResult = operation?.execute(context) + + if (operationResult?.isSuccess == true) { + Log_OC.d(TAG, "Folder ${folder.remotePath}: finished!") + } else { + Log_OC.d(TAG, "Folder ${folder.remotePath} failed!") + result = false + } + + folder.apply { + operationResult?.let { + internalFolderSyncResult = it.code.toString() + } + + internalFolderSyncTimestamp = System.currentTimeMillis() + } + + fileDataStorageManager.saveFile(folder) + } + } + + return if (result) { + Log_OC.d(TAG, "Worker finished with success!") + Result.success() + } else { + Log_OC.d(TAG, "Worker finished with failure!") + Result.failure() + } + } + + override fun onStopped() { + Log_OC.d(TAG, "OnStopped of worker called!") + operation?.cancel() + shouldRun = false + super.onStopped() + } + + @Suppress("TooGenericExceptionCaught") + private fun checkFreeSpace(folder: OCFile): Result? { + val storagePath = folder.storagePath ?: MainApp.getStoragePath() + val file = File(storagePath) + + if (!file.exists()) return null + + return try { + val freeSpaceLeft = file.freeSpace + val localFolder = File(storagePath, MainApp.getDataFolder()) + val localFolderSize = FileStorageUtils.getFolderSize(localFolder) + val remoteFolderSize = folder.fileLength + + if (freeSpaceLeft < (remoteFolderSize - localFolderSize)) { + Log_OC.d(TAG, "Not enough space left!") + Result.failure() + } else { + null + } + } catch (e: Exception) { + Log_OC.d(TAG, "Error caught at checkFreeSpace: $e") + null + } + } + + companion object { + const val TAG = "InternalTwoWaySyncWork" + } +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/JobInfo.kt b/app/src/main/java/com/nextcloud/client/jobs/JobInfo.kt index 935964c..b23b933 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/JobInfo.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/JobInfo.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs diff --git a/app/src/main/java/com/nextcloud/client/jobs/JobsModule.kt b/app/src/main/java/com/nextcloud/client/jobs/JobsModule.kt index f05e9a0..3f33145 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/JobsModule.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/JobsModule.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs @@ -27,9 +27,7 @@ class JobsModule { .build() val contextWrapper = object : ContextWrapper(context) { - override fun getApplicationContext(): Context { - return this - } + override fun getApplicationContext(): Context = this } WorkManager.initialize(contextWrapper, configuration) @@ -42,7 +40,5 @@ class JobsModule { workManager: WorkManager, clock: Clock, preferences: AppPreferences - ): BackgroundJobManager { - return BackgroundJobManagerImpl(workManager, clock, preferences) - } + ): BackgroundJobManager = BackgroundJobManagerImpl(workManager, clock, preferences) } diff --git a/app/src/main/java/com/nextcloud/client/jobs/MediaFoldersDetectionWork.kt b/app/src/main/java/com/nextcloud/client/jobs/MediaFoldersDetectionWork.kt index eec7370..354b8a1 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/MediaFoldersDetectionWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/MediaFoldersDetectionWork.kt @@ -8,7 +8,7 @@ * Copyright (C) 2018 Andy Scherzinger * Copyright (C) 2020 Chris Narkiewicz * - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs @@ -41,7 +41,7 @@ import com.owncloud.android.datamodel.MediaFoldersModel import com.owncloud.android.datamodel.MediaProvider import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.ui.activity.ManageAccountsActivity.PENDING_FOR_REMOVAL +import com.owncloud.android.ui.activity.ManageAccountsActivity import com.owncloud.android.ui.activity.SyncedFoldersActivity import com.owncloud.android.ui.notifications.NotificationUtils import com.owncloud.android.utils.SyncedFolderUtils @@ -73,7 +73,7 @@ class MediaFoldersDetectionWork constructor( private val randomIdGenerator = Random(clock.currentTime) - @Suppress("LongMethod", "ComplexMethod", "NestedBlockDepth") // legacy code + @Suppress("LongMethod", "ComplexMethod", "NestedBlockDepth", "ReturnCount") // legacy code override fun doWork(): Result { val arbitraryDataProvider: ArbitraryDataProvider = ArbitraryDataProviderImpl(context) val gson = Gson() @@ -134,7 +134,7 @@ class MediaFoldersDetectionWork constructor( val allUsers = userAccountManager.allUsers val activeUsers: MutableList = ArrayList() for (user in allUsers) { - if (!arbitraryDataProvider.getBooleanValue(user, PENDING_FOR_REMOVAL)) { + if (!arbitraryDataProvider.getBooleanValue(user, ManageAccountsActivity.PENDING_FOR_REMOVAL)) { activeUsers.add(user) } } @@ -190,6 +190,7 @@ class MediaFoldersDetectionWork constructor( gson.toJson(mediaFoldersModel) ) } + return Result.success() } diff --git a/app/src/main/java/com/nextcloud/client/jobs/NotificationWork.kt b/app/src/main/java/com/nextcloud/client/jobs/NotificationWork.kt index ea8b75c..f95e110 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/NotificationWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/NotificationWork.kt @@ -2,10 +2,11 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs +import android.Manifest import android.accounts.AuthenticatorException import android.accounts.OperationCanceledException import android.app.Activity @@ -14,10 +15,12 @@ import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.graphics.BitmapFactory import android.media.RingtoneManager import android.text.TextUtils import android.util.Base64 +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.work.Worker @@ -29,6 +32,7 @@ import com.nextcloud.client.integrations.deck.DeckApi import com.owncloud.android.R import com.owncloud.android.datamodel.DecryptedPushMessage import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.OwnCloudClientFactory import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.operations.RemoteOperation import com.owncloud.android.lib.common.utils.Log_OC @@ -223,8 +227,17 @@ class NotificationWork constructor( } .build() ) - val notificationManager = NotificationManagerCompat.from(context) - notificationManager.notify(notification.getNotificationId(), notificationBuilder.build()) + + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + Log_OC.w(this, "Missing permission to post notifications") + } else { + val notificationManager = NotificationManagerCompat.from(context) + notificationManager.notify(notification.getNotificationId(), notificationBuilder.build()) + } } @Suppress("TooGenericExceptionCaught") // legacy code @@ -236,8 +249,7 @@ class NotificationWork constructor( } val user = optionalUser.get() try { - val client = OwnCloudClientManagerFactory.getDefaultSingleton() - .getClientFor(user.toOwnCloudAccount(), context) + val client = OwnCloudClientFactory.createNextcloudClient(user, context) val result = GetNotificationRemoteOperation(decryptedPushMessage.nid) .execute(client) if (result.isSuccess) { @@ -287,6 +299,7 @@ class NotificationWork constructor( val user = optionalUser.get() val client = OwnCloudClientManagerFactory.getDefaultSingleton() .getClientFor(user.toOwnCloudAccount(), context) + val nextcloudClient = OwnCloudClientFactory.createNextcloudClient(user, context) val actionType = intent.getStringExtra(KEY_NOTIFICATION_ACTION_TYPE) val actionLink = intent.getStringExtra(KEY_NOTIFICATION_ACTION_LINK) val success: Boolean = if (!actionType.isNullOrEmpty() && !actionLink.isNullOrEmpty()) { @@ -294,7 +307,7 @@ class NotificationWork constructor( resultCode == HttpStatus.SC_OK || resultCode == HttpStatus.SC_ACCEPTED } else { DeleteNotificationRemoteOperation(numericNotificationId) - .execute(client).isSuccess + .execute(nextcloudClient).isSuccess } if (success) { if (oldNotification == null) { diff --git a/app/src/main/java/com/nextcloud/client/jobs/OfflineSyncWork.kt b/app/src/main/java/com/nextcloud/client/jobs/OfflineSyncWork.kt index d2ee5ce..26cddf0 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/OfflineSyncWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/OfflineSyncWork.kt @@ -6,7 +6,7 @@ * Copyright (C) 2018 Mario Danic * Copyright (C) 2020 Chris Narkiewicz * - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs @@ -28,7 +28,7 @@ import com.owncloud.android.utils.FileStorageUtils import java.io.File @Suppress("LongParameterList") // Legacy code -class OfflineSyncWork constructor( +class OfflineSyncWork( private val context: Context, params: WorkerParameters, private val contentResolver: ContentResolver, @@ -65,7 +65,7 @@ class OfflineSyncWork constructor( return } - val updatedEtag = checkEtagChanged(folderName, storageManager, user) ?: return + val updatedEtag = checkETagChanged(folderName, storageManager, user) ?: return // iterate over downloaded files val files = folder.listFiles { obj: File -> obj.isFile } @@ -77,7 +77,9 @@ class OfflineSyncWork constructor( user, true, context, - storageManager + storageManager, + true, + false ) synchronizeFileOperation.execute(context) } @@ -101,41 +103,39 @@ class OfflineSyncWork constructor( } /** - * @return new etag if changed, `null` otherwise + * @return new eTag if changed, `null` otherwise */ - private fun checkEtagChanged(folderName: String, storageManager: FileDataStorageManager, user: User): String? { - val ocFolder = storageManager.getFileByPath(folderName) ?: return null + private fun checkETagChanged(folderName: String, storageManager: FileDataStorageManager, user: User): String? { + val folder = storageManager.getFileByEncryptedRemotePath(folderName) ?: return null - Log_OC.d(TAG, "$folderName: currentEtag: ${ocFolder.etag}") + Log_OC.d(TAG, "$folderName: current eTag: ${folder.etag}") // check for etag change, if false, skip - val checkEtagOperation = CheckEtagRemoteOperation( - ocFolder.remotePath, - ocFolder.etagOnServer - ) - val result = checkEtagOperation.execute(user, context) + val operation = CheckEtagRemoteOperation(folder.remotePath, folder.etagOnServer) + val result = operation.execute(user, context) + return when (result.code) { ResultCode.ETAG_UNCHANGED -> { Log_OC.d(TAG, "$folderName: eTag unchanged") null } ResultCode.FILE_NOT_FOUND -> { - val removalResult = storageManager.removeFolder(ocFolder, true, true) + val removalResult = storageManager.removeFolder(folder, true, true) if (!removalResult) { - Log_OC.e(TAG, "removal of " + ocFolder.storagePath + " failed: file not found") + Log_OC.e(TAG, "removal of " + folder.storagePath + " failed: file not found") } null } ResultCode.ETAG_CHANGED -> { Log_OC.d(TAG, "$folderName: eTag changed") - result.data[0] as String + result?.data?.get(0) as? String } else -> if (connectivityService.isInternetWalled) { Log_OC.d(TAG, "No connectivity, skipping sync") null } else { Log_OC.d(TAG, "$folderName: eTag changed") - result.data[0] as String + result?.data?.get(0) as? String } } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/TestJob.kt b/app/src/main/java/com/nextcloud/client/jobs/TestJob.kt index c30f2cb..2d70042 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/TestJob.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/TestJob.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs @@ -11,11 +11,8 @@ import androidx.work.Data import androidx.work.Worker import androidx.work.WorkerParameters -class TestJob( - appContext: Context, - params: WorkerParameters, - private val backgroundJobManager: BackgroundJobManager -) : Worker(appContext, params) { +class TestJob(appContext: Context, params: WorkerParameters, private val backgroundJobManager: BackgroundJobManager) : + Worker(appContext, params) { companion object { private const val MAX_PROGRESS = 100 diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt new file mode 100644 index 0000000..a43b524 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadHelper.kt @@ -0,0 +1,135 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.autoUpload + +import com.nextcloud.utils.extensions.shouldSkipFile +import com.nextcloud.utils.extensions.toLocalPath +import com.owncloud.android.datamodel.FilesystemDataProvider +import com.owncloud.android.datamodel.SyncedFolder +import com.owncloud.android.lib.common.utils.Log_OC +import java.io.IOException +import java.nio.file.AccessDeniedException +import java.nio.file.FileVisitOption +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes + +@Suppress("TooGenericExceptionCaught", "MagicNumber", "ReturnCount") +class AutoUploadHelper { + companion object { + private const val TAG = "AutoUploadHelper" + private const val MAX_DEPTH = 100 + } + + fun insertCustomFolderIntoDB(folder: SyncedFolder, filesystemDataProvider: FilesystemDataProvider?): Int { + val path = Paths.get(folder.localPath) + + if (!Files.exists(path)) { + Log_OC.w(TAG, "Folder does not exist: ${folder.localPath}") + return 0 + } + + if (!Files.isReadable(path)) { + Log_OC.w(TAG, "Folder is not readable: ${folder.localPath}") + return 0 + } + + val excludeHidden = folder.isExcludeHidden + + var fileCount = 0 + var skipCount = 0 + var errorCount = 0 + + try { + Files.walkFileTree( + path, + setOf(FileVisitOption.FOLLOW_LINKS), + MAX_DEPTH, + object : SimpleFileVisitor() { + + override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes?): FileVisitResult { + if (excludeHidden && dir != path && dir.toFile().isHidden) { + Log_OC.d(TAG, "Skipping hidden directory: ${dir.fileName}") + skipCount++ + return FileVisitResult.SKIP_SUBTREE + } + + return FileVisitResult.CONTINUE + } + + override fun visitFile(file: Path, attrs: BasicFileAttributes?): FileVisitResult { + try { + val javaFile = file.toFile() + val lastModified = attrs?.lastModifiedTime()?.toMillis() ?: javaFile.lastModified() + val creationTime = attrs?.creationTime()?.toMillis() + + if (folder.shouldSkipFile(javaFile, lastModified, creationTime)) { + skipCount++ + return FileVisitResult.CONTINUE + } + + val localPath = file.toLocalPath() + + filesystemDataProvider?.storeOrUpdateFileValue( + localPath, + lastModified, + javaFile.isDirectory, + folder + ) + + fileCount++ + + if (fileCount % 100 == 0) { + Log_OC.d(TAG, "Processed $fileCount files so far...") + } + } catch (e: Exception) { + Log_OC.e(TAG, "Error processing file: $file", e) + errorCount++ + } + + return FileVisitResult.CONTINUE + } + + override fun visitFileFailed(file: Path, exc: IOException?): FileVisitResult { + when (exc) { + is AccessDeniedException -> { + Log_OC.w(TAG, "Access denied: $file") + } + else -> { + Log_OC.e(TAG, "Failed to visit file: $file", exc) + } + } + errorCount++ + return FileVisitResult.CONTINUE + } + + override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult { + if (exc != null) { + Log_OC.e(TAG, "Error after visiting directory: $dir", exc) + errorCount++ + } + return FileVisitResult.CONTINUE + } + } + ) + + Log_OC.d( + TAG, + "Scan complete for ${folder.localPath}: " + + "$fileCount files processed, $skipCount skipped, $errorCount errors" + ) + } catch (e: Exception) { + Log_OC.e(TAG, "Error walking file tree: ${folder.localPath}", e) + } + + return fileCount + } +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt new file mode 100644 index 0000000..2bef1d4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/AutoUploadWorker.kt @@ -0,0 +1,480 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.autoUpload + +import android.app.Notification +import android.app.NotificationManager +import android.content.Context +import android.content.res.Resources +import androidx.core.app.NotificationCompat +import androidx.exifinterface.media.ExifInterface +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.nextcloud.client.account.User +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.database.entity.UploadEntity +import com.nextcloud.client.database.entity.toOCUpload +import com.nextcloud.client.database.entity.toUploadEntity +import com.nextcloud.client.device.PowerManagementService +import com.nextcloud.client.jobs.BackgroundJobManager +import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.nextcloud.client.network.ConnectivityService +import com.nextcloud.client.preferences.SubFolderRule +import com.nextcloud.utils.ForegroundServiceHelper +import com.nextcloud.utils.extensions.updateStatus +import com.owncloud.android.R +import com.owncloud.android.datamodel.ArbitraryDataProviderImpl +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.ForegroundServiceType +import com.owncloud.android.datamodel.MediaFolderType +import com.owncloud.android.datamodel.SyncedFolder +import com.owncloud.android.datamodel.SyncedFolderProvider +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.db.OCUpload +import com.owncloud.android.lib.common.OwnCloudAccount +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.UploadFileOperation +import com.owncloud.android.ui.activity.SettingsActivity +import com.owncloud.android.ui.notifications.NotificationUtils +import com.owncloud.android.utils.FileStorageUtils +import com.owncloud.android.utils.FilesSyncHelper +import com.owncloud.android.utils.MimeType +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.text.ParsePosition +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +@Suppress("LongParameterList", "TooManyFunctions") +class AutoUploadWorker( + private val context: Context, + params: WorkerParameters, + private val userAccountManager: UserAccountManager, + private val uploadsStorageManager: UploadsStorageManager, + private val connectivityService: ConnectivityService, + private val powerManagementService: PowerManagementService, + private val syncedFolderProvider: SyncedFolderProvider, + private val backgroundJobManager: BackgroundJobManager, + private val repository: FileSystemRepository +) : CoroutineWorker(context, params) { + + companion object { + const val TAG = "🔄📤" + "AutoUpload" + const val OVERRIDE_POWER_SAVING = "overridePowerSaving" + const val CONTENT_URIS = "content_uris" + const val SYNCED_FOLDER_ID = "syncedFolderId" + private const val CHANNEL_ID = NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD + + private const val NOTIFICATION_ID = 266 + } + + private val helper = AutoUploadHelper() + private lateinit var syncedFolder: SyncedFolder + private val notificationManager by lazy { + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + } + + @Suppress("TooGenericExceptionCaught", "ReturnCount") + override suspend fun doWork(): Result { + return try { + val syncFolderId = inputData.getLong(SYNCED_FOLDER_ID, -1) + syncedFolder = syncedFolderProvider.getSyncedFolderByID(syncFolderId) + ?.takeIf { it.isEnabled } ?: return Result.failure() + + // initial notification + val notification = createNotification(context.getString(R.string.upload_files)) + updateForegroundInfo(notification) + + /** + * Receives from [com.nextcloud.client.jobs.ContentObserverWork.checkAndTriggerAutoUpload] + */ + val contentUris = inputData.getStringArray(CONTENT_URIS) + + if (canExitEarly(contentUris, syncFolderId)) { + return Result.retry() + } + + collectFileChangesFromContentObserverWork(contentUris) + updateNotification() + uploadFiles(syncedFolder) + + Log_OC.d(TAG, "✅ ${syncedFolder.remotePath} finished checking files.") + Result.success() + } catch (e: Exception) { + Log_OC.e(TAG, "❌ failed: ${e.message}") + Result.failure() + } + } + + private fun updateNotification() { + getStartNotificationTitle()?.let { (localFolderName, remoteFolderName) -> + val startNotification = createNotification( + context.getString( + R.string.auto_upload_worker_start_text, + localFolderName, + remoteFolderName + ) + ) + + notificationManager.notify(NOTIFICATION_ID, startNotification) + } + } + + private suspend fun updateForegroundInfo(notification: Notification) { + val foregroundInfo = ForegroundServiceHelper.createWorkerForegroundInfo( + NOTIFICATION_ID, + notification, + ForegroundServiceType.DataSync + ) + setForeground(foregroundInfo) + } + + private fun createNotification(title: String): Notification = NotificationCompat.Builder(context, CHANNEL_ID) + .setContentTitle(title) + .setSmallIcon(R.drawable.uploads) + .setOngoing(true) + .setSound(null) + .setVibrate(null) + .setOnlyAlertOnce(true) + .setSilent(true) + .build() + + @Suppress("TooGenericExceptionCaught") + private fun getStartNotificationTitle(): Pair? = try { + val localPath = syncedFolder.localPath + val remotePath = syncedFolder.remotePath + if (localPath.isBlank() || remotePath.isBlank()) { + null + } else { + try { + File(localPath).name to File(remotePath).name + } catch (_: Exception) { + null + } + } + } catch (_: Exception) { + null + } + + @Suppress("ReturnCount") + private fun canExitEarly(contentUris: Array?, syncedFolderID: Long): Boolean { + val overridePowerSaving = inputData.getBoolean(OVERRIDE_POWER_SAVING, false) + if ((powerManagementService.isPowerSavingEnabled && !overridePowerSaving)) { + Log_OC.w(TAG, "⚡ Skipping: device is in power saving mode") + return true + } + + if (syncedFolderID < 0) { + Log_OC.e(TAG, "invalid sync folder id") + return true + } + + if (backgroundJobManager.bothFilesSyncJobsRunning(syncedFolderID)) { + Log_OC.w(TAG, "🚧 another worker is already running for $syncedFolderID") + return true + } + + val totalScanInterval = syncedFolder.getTotalScanInterval(connectivityService, powerManagementService) + val currentTime = System.currentTimeMillis() + val passedScanInterval = totalScanInterval <= currentTime + + Log_OC.d(TAG, "lastScanTimestampMs: " + syncedFolder.lastScanTimestampMs) + Log_OC.d(TAG, "totalScanInterval: $totalScanInterval") + Log_OC.d(TAG, "currentTime: $currentTime") + Log_OC.d(TAG, "passedScanInterval: $passedScanInterval") + + if (!passedScanInterval && contentUris.isNullOrEmpty() && !overridePowerSaving) { + Log_OC.w( + TAG, + "skipped since started before scan interval and nothing todo: " + syncedFolder.localPath + ) + return true + } + + return false + } + + /** + * Instead of scanning the entire local folder, optional content URIs can be passed to the worker + * to detect only the relevant changes. + */ + @Suppress("MagicNumber", "TooGenericExceptionCaught") + private suspend fun collectFileChangesFromContentObserverWork(contentUris: Array?) = try { + withContext(Dispatchers.IO) { + if (contentUris.isNullOrEmpty()) { + FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, helper) + } else { + val isContentUrisStored = FilesSyncHelper.insertChangedEntries(syncedFolder, contentUris) + if (!isContentUrisStored) { + Log_OC.w( + TAG, + "changed content uris not stored, fallback to insert all db entries to not lose files" + ) + + FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, helper) + } + } + syncedFolder.lastScanTimestampMs = System.currentTimeMillis() + syncedFolderProvider.updateSyncFolder(syncedFolder) + } + } catch (e: Exception) { + Log_OC.d(TAG, "Exception collectFileChangesFromContentObserverWork: $e") + } + + private fun prepareDateFormat(): SimpleDateFormat { + val currentLocale = context.resources.configuration.locales[0] + return SimpleDateFormat("yyyy:MM:dd HH:mm:ss", currentLocale).apply { + timeZone = TimeZone.getTimeZone(TimeZone.getDefault().id) + } + } + + private fun getUserOrReturn(syncedFolder: SyncedFolder): User? { + val optionalUser = userAccountManager.getUser(syncedFolder.account) + if (!optionalUser.isPresent) { + Log_OC.w(TAG, "user not present") + return null + } + return optionalUser.get() + } + + @Suppress("DEPRECATION") + private fun getUploadSettings(syncedFolder: SyncedFolder): Triple { + val lightVersion = context.resources.getBoolean(R.bool.syncedFolder_light) + val accountName = syncedFolder.account + + return if (lightVersion) { + Log_OC.d(TAG, "light version is used") + val arbitraryDataProvider = ArbitraryDataProviderImpl(context) + val needsCharging = context.resources.getBoolean(R.bool.syncedFolder_light_on_charging) + val needsWifi = arbitraryDataProvider.getBooleanValue( + accountName, + SettingsActivity.SYNCED_FOLDER_LIGHT_UPLOAD_ON_WIFI + ) + val uploadActionString = context.resources.getString(R.string.syncedFolder_light_upload_behaviour) + val uploadAction = getUploadAction(uploadActionString) + Log_OC.d(TAG, "upload action is: $uploadAction") + Triple(needsCharging, needsWifi, uploadAction) + } else { + Log_OC.d(TAG, "not light version is used") + Triple(syncedFolder.isChargingOnly, syncedFolder.isWifiOnly, syncedFolder.uploadAction) + } + } + + @Suppress("LongMethod", "DEPRECATION", "TooGenericExceptionCaught") + private suspend fun uploadFiles(syncedFolder: SyncedFolder) = withContext(Dispatchers.IO) { + val dateFormat = prepareDateFormat() + val user = getUserOrReturn(syncedFolder) ?: return@withContext + val ocAccount = OwnCloudAccount(user.toPlatformAccount(), context) + val client = OwnCloudClientManagerFactory.getDefaultSingleton() + .getClientFor(ocAccount, context) + val lightVersion = context.resources.getBoolean(R.bool.syncedFolder_light) + val currentLocale = context.resources.configuration.locales[0] + + var lastId = 0 + while (true) { + val filePathsWithIds = repository.getFilePathsWithIds(syncedFolder, lastId) + + if (filePathsWithIds.isEmpty()) { + Log_OC.w(TAG, "no more files to upload at lastId: $lastId") + break + } + Log_OC.d(TAG, "Processing batch: lastId=$lastId, count=${filePathsWithIds.size}") + + filePathsWithIds.forEach { (path, id) -> + val file = File(path) + val localPath = file.absolutePath + val remotePath = getRemotePath( + file, + syncedFolder, + dateFormat, + lightVersion, + context.resources, + currentLocale + ) + + try { + var (uploadEntity, upload) = createEntityAndUpload(user, localPath, remotePath) + try { + // Insert/update to IN_PROGRESS state before starting upload + val generatedId = uploadsStorageManager.uploadDao.insertOrReplace(uploadEntity) + uploadEntity = uploadEntity.copy(id = generatedId.toInt()) + upload.uploadId = generatedId + + val operation = createUploadFileOperation(upload, user) + Log_OC.d(TAG, "🕒 uploading: $localPath, id: $generatedId") + + val result = operation.execute(client) + uploadsStorageManager.updateStatus(uploadEntity, result.isSuccess) + + if (result.isSuccess) { + repository.markFileAsUploaded(localPath, syncedFolder) + Log_OC.d(TAG, "✅ upload completed: $localPath") + } else { + Log_OC.e( + TAG, + "❌ upload failed $localPath (${upload.accountName}): ${result.logMessage}" + ) + } + } catch (e: Exception) { + uploadsStorageManager.updateStatus( + uploadEntity, + UploadsStorageManager.UploadStatus.UPLOAD_FAILED + ) + Log_OC.e( + TAG, + "Exception during upload file, localPath: $localPath, remotePath: $remotePath," + + " exception: $e" + ) + } + } catch (e: Exception) { + Log_OC.e( + TAG, + "Exception uploadFiles during creating entity and upload, localPath: $localPath, " + + "remotePath: $remotePath, exception: $e" + ) + } + + // update last id so upload can continue where it left + lastId = id + } + } + } + + private fun createEntityAndUpload(user: User, localPath: String, remotePath: String): Pair { + val (needsCharging, needsWifi, uploadAction) = getUploadSettings(syncedFolder) + Log_OC.d(TAG, "creating oc upload for ${user.accountName}") + + // Get existing upload or create new one + val uploadEntity = uploadsStorageManager.uploadDao.getUploadByAccountAndPaths( + localPath = localPath, + remotePath = remotePath, + accountName = user.accountName + ) + + val upload = ( + uploadEntity?.toOCUpload(null) ?: OCUpload( + localPath, + remotePath, + user.accountName + ) + ).apply { + uploadStatus = UploadsStorageManager.UploadStatus.UPLOAD_IN_PROGRESS + nameCollisionPolicy = syncedFolder.nameCollisionPolicy + isUseWifiOnly = needsWifi + isWhileChargingOnly = needsCharging + localAction = uploadAction + + // Only set these for new uploads + if (uploadEntity == null) { + createdBy = UploadFileOperation.CREATED_AS_INSTANT_PICTURE + isCreateRemoteFolder = true + } + } + + return upload.toUploadEntity() to upload + } + + private fun createUploadFileOperation(upload: OCUpload, user: User): UploadFileOperation = UploadFileOperation( + uploadsStorageManager, + connectivityService, + powerManagementService, + user, + null, + upload, + upload.nameCollisionPolicy, + upload.localAction, + context, + upload.isUseWifiOnly, + upload.isWhileChargingOnly, + true, + FileDataStorageManager(user, context.contentResolver) + ) + + private fun getRemotePath( + file: File, + syncedFolder: SyncedFolder, + sFormatter: SimpleDateFormat, + lightVersion: Boolean, + resources: Resources, + currentLocale: Locale + ): String { + val lastModificationTime = calculateLastModificationTime(file, syncedFolder, sFormatter) + + val (remoteFolder, useSubfolders, subFolderRule) = if (lightVersion) { + Triple( + resources.getString(R.string.syncedFolder_remote_folder), + resources.getBoolean(R.bool.syncedFolder_light_use_subfolders), + SubFolderRule.YEAR_MONTH + ) + } else { + Triple( + syncedFolder.remotePath, + syncedFolder.isSubfolderByDate, + syncedFolder.subfolderRule + ) + } + + return FileStorageUtils.getInstantUploadFilePath( + file, + currentLocale, + remoteFolder, + syncedFolder.localPath, + lastModificationTime, + useSubfolders, + subFolderRule + ) + } + + private fun hasExif(file: File): Boolean { + val mimeType = FileStorageUtils.getMimeTypeFromName(file.absolutePath) + return MimeType.JPEG.equals(mimeType, ignoreCase = true) || MimeType.TIFF.equals(mimeType, ignoreCase = true) + } + + @Suppress("NestedBlockDepth") + private fun calculateLastModificationTime( + file: File, + syncedFolder: SyncedFolder, + formatter: SimpleDateFormat + ): Long { + var lastModificationTime = file.lastModified() + if (MediaFolderType.IMAGE == syncedFolder.type && hasExif(file)) { + Log_OC.d(TAG, "calculateLastModificationTime exif found") + + @Suppress("TooGenericExceptionCaught") + try { + val exifInterface = ExifInterface(file.absolutePath) + val exifDate = exifInterface.getAttribute(ExifInterface.TAG_DATETIME) + if (!exifDate.isNullOrBlank()) { + val pos = ParsePosition(0) + val dateTime = formatter.parse(exifDate, pos) + if (dateTime != null) { + lastModificationTime = dateTime.time + Log_OC.w(TAG, "calculateLastModificationTime calculatedTime is: $lastModificationTime") + } else { + Log_OC.w(TAG, "calculateLastModificationTime dateTime is empty") + } + } else { + Log_OC.w(TAG, "calculateLastModificationTime exifDate is empty") + } + } catch (e: Exception) { + Log_OC.d(TAG, "Failed to get the proper time " + e.localizedMessage) + } + } + return lastModificationTime + } + + private fun getUploadAction(action: String): Int = when (action) { + "LOCAL_BEHAVIOUR_FORGET" -> FileUploadWorker.Companion.LOCAL_BEHAVIOUR_FORGET + "LOCAL_BEHAVIOUR_MOVE" -> FileUploadWorker.Companion.LOCAL_BEHAVIOUR_MOVE + "LOCAL_BEHAVIOUR_DELETE" -> FileUploadWorker.Companion.LOCAL_BEHAVIOUR_DELETE + else -> FileUploadWorker.Companion.LOCAL_BEHAVIOUR_FORGET + } +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt new file mode 100644 index 0000000..a4bf50a --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/autoUpload/FileSystemRepository.kt @@ -0,0 +1,66 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.autoUpload + +import com.nextcloud.client.database.dao.FileSystemDao +import com.owncloud.android.datamodel.SyncedFolder +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.utils.SyncedFolderUtils +import java.io.File + +class FileSystemRepository(private val dao: FileSystemDao) { + + companion object { + private const val TAG = "FilesystemRepository" + const val BATCH_SIZE = 50 + } + + @Suppress("NestedBlockDepth") + suspend fun getFilePathsWithIds(syncedFolder: SyncedFolder, lastId: Int): List> { + val syncedFolderId = syncedFolder.id.toString() + Log_OC.d(TAG, "Fetching candidate files for syncedFolderId = $syncedFolderId") + + val entities = dao.getAutoUploadFilesEntities(syncedFolderId, BATCH_SIZE, lastId) + val filtered = mutableListOf>() + + entities.forEach { + it.localPath?.let { path -> + val file = File(path) + if (!file.exists()) { + Log_OC.w(TAG, "Ignoring file for upload (doesn't exist): $path") + } else if (!SyncedFolderUtils.isQualifiedFolder(file.parent)) { + Log_OC.w(TAG, "Ignoring file for upload (unqualified folder): $path") + } else if (!SyncedFolderUtils.isFileNameQualifiedForAutoUpload(file.name)) { + Log_OC.w(TAG, "Ignoring file for upload (unqualified file): $path") + } else { + Log_OC.d(TAG, "Adding path to upload: $path") + + if (it.id != null) { + filtered.add(path to it.id) + } else { + Log_OC.w(TAG, "cant adding path to upload, id is null") + } + } + } + } + + return filtered + } + + @Suppress("TooGenericExceptionCaught") + suspend fun markFileAsUploaded(localPath: String, syncedFolder: SyncedFolder) { + val syncedFolderIdStr = syncedFolder.id.toString() + + try { + dao.markFileAsUploaded(localPath, syncedFolderIdStr) + Log_OC.d(TAG, "Marked file as uploaded: $localPath for syncedFolderId=$syncedFolderIdStr") + } catch (e: Exception) { + Log_OC.e(TAG, "Error marking file as uploaded: ${e.message}", e) + } + } +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/clipboard/ClipboardClearWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/clipboard/ClipboardClearWorker.kt new file mode 100644 index 0000000..5adc17d --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/clipboard/ClipboardClearWorker.kt @@ -0,0 +1,49 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.clipboard + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.os.Build +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.owncloud.android.lib.common.utils.Log_OC + +class ClipboardClearWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) { + private val tag = ClipboardClearWorker::class.java.name + + companion object { + const val CLIPBOARD_TEXT = "clipboard_text" + } + + @Suppress("TooGenericExceptionCaught", "ReturnCount") + override fun doWork(): Result { + try { + val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val currentClip = clipboardManager.primaryClip ?: return Result.success() + val clipboardText = currentClip.getItemAt(0).text?.toString() ?: return Result.success() + val copiedText = inputData.getString(CLIPBOARD_TEXT) + if (copiedText != clipboardText) { + return Result.success() + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + clipboardManager.clearPrimaryClip() + } else { + val newEmptyClip = ClipData.newPlainText("EmptyClipContent", "") + clipboardManager.setPrimaryClip(newEmptyClip) + } + + return Result.success() + } catch (e: Exception) { + Log_OC.e(tag, "Error in clipboard clear worker", e) + return Result.retry() + } + } +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/download/DownloadNotificationManager.kt b/app/src/main/java/com/nextcloud/client/jobs/download/DownloadNotificationManager.kt index a656ad8..9ccb36b 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/download/DownloadNotificationManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/download/DownloadNotificationManager.kt @@ -1,24 +1,18 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs.download -import android.app.Notification -import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.graphics.BitmapFactory -import android.os.Build -import android.os.Handler -import android.os.Looper -import androidx.core.app.NotificationCompat +import com.nextcloud.client.jobs.notification.WorkerNotificationManager +import com.nextcloud.utils.numberFormatter.NumberFormatter import com.owncloud.android.R -import com.owncloud.android.lib.resources.files.FileUtils import com.owncloud.android.operations.DownloadFileOperation import com.owncloud.android.ui.notifications.NotificationUtils import com.owncloud.android.utils.theme.ViewThemeUtils @@ -26,52 +20,28 @@ import java.io.File import java.security.SecureRandom @Suppress("TooManyFunctions") -class DownloadNotificationManager( - private val id: Int, - private val context: Context, - private val viewThemeUtils: ViewThemeUtils -) { - private var notification: Notification - private var notificationBuilder: NotificationCompat.Builder - private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager +class DownloadNotificationManager(id: Int, private val context: Context, viewThemeUtils: ViewThemeUtils) : + WorkerNotificationManager( + id, + context, + viewThemeUtils, + tickerId = R.string.downloader_download_in_progress_ticker, + channelId = NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD + ) { - init { - notificationBuilder = NotificationUtils.newNotificationBuilder(context, viewThemeUtils).apply { - setContentTitle(context.getString(R.string.downloader_download_in_progress_ticker)) - setTicker(context.getString(R.string.downloader_download_in_progress_ticker)) - setSmallIcon(R.drawable.notification_icon) - setLargeIcon(BitmapFactory.decodeResource(context.resources, R.drawable.notification_icon)) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD) - } - } - - notification = notificationBuilder.build() - } + private var lastPercent = -1 @Suppress("MagicNumber") fun prepareForStart(operation: DownloadFileOperation) { - notificationBuilder = NotificationUtils.newNotificationBuilder(context, viewThemeUtils).apply { - setSmallIcon(R.drawable.notification_icon) - setOngoing(true) + currentOperationTitle = File(operation.savePath).name + + notificationBuilder.run { + setContentTitle(currentOperationTitle) + setOngoing(false) setProgress(100, 0, operation.size < 0) - setContentText( - String.format( - context.getString(R.string.downloader_download_in_progress), 0, - File(operation.savePath).name - ) - ) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD) - } - - notificationManager.notify( - id, - this.build() - ) } + + showNotification() } fun prepareForResult() { @@ -82,23 +52,21 @@ class DownloadNotificationManager( } @Suppress("MagicNumber") - fun updateDownloadProgress(filePath: String, percent: Int, totalToTransfer: Long) { - notificationBuilder.run { - setProgress(100, percent, totalToTransfer < 0) - val fileName: String = filePath.substring(filePath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1) - val text = - String.format(context.getString(R.string.downloader_download_in_progress), percent, fileName) - val title = - context.getString(R.string.downloader_download_in_progress_ticker) - updateNotificationText(title, text) + fun updateDownloadProgress(percent: Int, totalToTransfer: Long) { + // If downloads are so fast, no need to notify again. + if (percent == lastPercent) { + return } + lastPercent = percent + + val progressText = NumberFormatter.getPercentageText(percent) + setProgress(percent, progressText, totalToTransfer < 0) + showNotification() } @Suppress("MagicNumber") fun dismissNotification() { - Handler(Looper.getMainLooper()).postDelayed({ - notificationManager.cancel(id) - }, 2000) + dismissNotification(2000) } fun showNewNotification(text: String) { @@ -106,24 +74,12 @@ class DownloadNotificationManager( notificationBuilder.run { setProgress(0, 0, false) - setContentTitle(null) - setContentText(text) + setContentTitle(text) setOngoing(false) notificationManager.notify(notifyId, this.build()) } } - private fun updateNotificationText(title: String?, text: String) { - notificationBuilder.run { - title?.let { - setContentTitle(title) - } - - setContentText(text) - notificationManager.notify(id, this.build()) - } - } - fun setContentIntent(intent: Intent, flag: Int) { notificationBuilder.setContentIntent( PendingIntent.getActivity( @@ -134,12 +90,4 @@ class DownloadNotificationManager( ) ) } - - fun getId(): Int { - return id - } - - fun getNotification(): Notification { - return notificationBuilder.build() - } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/download/DownloadTask.kt b/app/src/main/java/com/nextcloud/client/jobs/download/DownloadTask.kt index 692f869..b8708df 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/download/DownloadTask.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/download/DownloadTask.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs.download @@ -45,9 +45,7 @@ class DownloadTask( private val clientProvider: () -> OwnCloudClient, private val contentResolver: ContentResolver ) { - fun create(): DownloadTask { - return DownloadTask(context, contentResolver, clientProvider) - } + fun create(): DownloadTask = DownloadTask(context, contentResolver, clientProvider) } // Unused progress, isCancelled arguments needed for TransferManagerTest diff --git a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadError.kt b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadError.kt index f6a2d06..cb027e9 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadError.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadError.kt @@ -1,12 +1,13 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs.download enum class FileDownloadError { - Failed, Cancelled + Failed, + Cancelled } diff --git a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadHelper.kt index 664afee..76f4b99 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadHelper.kt @@ -1,18 +1,20 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs.download import com.nextcloud.client.account.User import com.nextcloud.client.jobs.BackgroundJobManager +import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker import com.owncloud.android.MainApp import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.DownloadFileOperation import com.owncloud.android.operations.DownloadType import com.owncloud.android.utils.MimeTypeUtil @@ -29,11 +31,10 @@ class FileDownloadHelper { companion object { private var instance: FileDownloadHelper? = null + private const val TAG = "FileDownloadHelper" - fun instance(): FileDownloadHelper { - return instance ?: synchronized(this) { - instance ?: FileDownloadHelper().also { instance = it } - } + fun instance(): FileDownloadHelper = instance ?: synchronized(this) { + instance ?: FileDownloadHelper().also { instance = it } } } @@ -46,12 +47,8 @@ class FileDownloadHelper { return false } - val fileStorageManager = FileDataStorageManager(user, MainApp.getAppContext().contentResolver) - val topParentId = fileStorageManager.getTopParentId(file) - - val isJobScheduled = backgroundJobManager.isStartFileDownloadJobScheduled(user, file.fileId) - return isJobScheduled || if (file.isFolder) { - backgroundJobManager.isStartFileDownloadJobScheduled(user, topParentId) + return if (file.isFolder) { + FolderDownloadWorker.isDownloading(file.fileId) } else { FileDownloadWorker.isDownloading(user.accountName, file.fileId) } @@ -81,11 +78,7 @@ class FileDownloadHelper { backgroundJobManager.cancelFilesDownloadJob(currentUser, currentFile.fileId) } - fun saveFile( - file: OCFile, - currentDownload: DownloadFileOperation?, - storageManager: FileDataStorageManager? - ) { + fun saveFile(file: OCFile, currentDownload: DownloadFileOperation?, storageManager: FileDataStorageManager?) { val syncDate = System.currentTimeMillis() file.apply { @@ -145,4 +138,14 @@ class FileDownloadHelper { conflictUploadId ) } + + fun downloadFolder(folder: OCFile?, accountName: String) { + if (folder == null) { + Log_OC.e(TAG, "folder cannot be null, cant sync") + return + } + backgroundJobManager.downloadFolder(folder, accountName) + } + + fun cancelFolderDownload() = backgroundJobManager.cancelFolderDownload() } diff --git a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadIntents.kt b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadIntents.kt index 26f3a15..5077122 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadIntents.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadIntents.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs.download @@ -22,63 +22,53 @@ import com.owncloud.android.ui.preview.PreviewImageFragment class FileDownloadIntents(private val context: Context) { - fun newDownloadIntent( - download: DownloadFileOperation, - linkedToRemotePath: String - ): Intent { - return Intent(FileDownloadWorker.getDownloadAddedMessage()).apply { + fun newDownloadIntent(download: DownloadFileOperation, linkedToRemotePath: String): Intent = + Intent(FileDownloadWorker.getDownloadAddedMessage()).apply { putExtra(FileDownloadWorker.EXTRA_ACCOUNT_NAME, download.user.accountName) putExtra(FileDownloadWorker.EXTRA_REMOTE_PATH, download.remotePath) putExtra(FileDownloadWorker.EXTRA_LINKED_TO_PATH, linkedToRemotePath) setPackage(context.packageName) } - } fun downloadFinishedIntent( download: DownloadFileOperation, downloadResult: RemoteOperationResult<*>, unlinkedFromRemotePath: String? - ): Intent { - return Intent(FileDownloadWorker.getDownloadFinishMessage()).apply { - putExtra(FileDownloadWorker.EXTRA_DOWNLOAD_RESULT, downloadResult.isSuccess) - putExtra(FileDownloadWorker.EXTRA_ACCOUNT_NAME, download.user.accountName) - putExtra(FileDownloadWorker.EXTRA_REMOTE_PATH, download.remotePath) - putExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR, download.behaviour) - putExtra(SendShareDialog.ACTIVITY_NAME, download.activityName) - putExtra(SendShareDialog.PACKAGE_NAME, download.packageName) - if (unlinkedFromRemotePath != null) { - putExtra(FileDownloadWorker.EXTRA_LINKED_TO_PATH, unlinkedFromRemotePath) - } - setPackage(context.packageName) + ): Intent = Intent(FileDownloadWorker.getDownloadFinishMessage()).apply { + putExtra(FileDownloadWorker.EXTRA_DOWNLOAD_RESULT, downloadResult.isSuccess) + putExtra(FileDownloadWorker.EXTRA_ACCOUNT_NAME, download.user.accountName) + putExtra(FileDownloadWorker.EXTRA_REMOTE_PATH, download.remotePath) + putExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR, download.behaviour) + putExtra(SendShareDialog.ACTIVITY_NAME, download.activityName) + putExtra(SendShareDialog.PACKAGE_NAME, download.packageName) + if (unlinkedFromRemotePath != null) { + putExtra(FileDownloadWorker.EXTRA_LINKED_TO_PATH, unlinkedFromRemotePath) } + setPackage(context.packageName) } - fun credentialContentIntent(user: User): Intent { - return Intent(context, AuthenticatorActivity::class.java).apply { - putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, user.toPlatformAccount()) - putExtra( - AuthenticatorActivity.EXTRA_ACTION, - AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN - ) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) - addFlags(Intent.FLAG_FROM_BACKGROUND) - } + fun credentialContentIntent(user: User): Intent = Intent(context, AuthenticatorActivity::class.java).apply { + putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, user.toPlatformAccount()) + putExtra( + AuthenticatorActivity.EXTRA_ACTION, + AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN + ) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) + addFlags(Intent.FLAG_FROM_BACKGROUND) } - fun detailsIntent(operation: DownloadFileOperation?): Intent { - return if (operation != null) { - if (PreviewImageFragment.canBePreviewed(operation.file)) { - Intent(context, PreviewImageActivity::class.java) - } else { - Intent(context, FileDisplayActivity::class.java) - }.apply { - putExtra(FileActivity.EXTRA_FILE, operation.file) - putExtra(FileActivity.EXTRA_USER, operation.user) - flags = Intent.FLAG_ACTIVITY_CLEAR_TOP - } + fun detailsIntent(operation: DownloadFileOperation?): Intent = if (operation != null) { + if (PreviewImageFragment.canBePreviewed(operation.file)) { + Intent(context, PreviewImageActivity::class.java) } else { - Intent() + Intent(context, FileDisplayActivity::class.java) + }.apply { + putExtra(FileActivity.EXTRA_FILE, operation.file) + putExtra(FileActivity.EXTRA_USER, operation.user) + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP } + } else { + Intent() } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt index 5da4fc1..e160e1d 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadWorker.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs.download @@ -16,13 +16,15 @@ import android.util.Pair import androidx.core.util.component1 import androidx.core.util.component2 import androidx.localbroadcastmanager.content.LocalBroadcastManager -import androidx.work.Worker +import androidx.work.CoroutineWorker +import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager import com.nextcloud.model.WorkerState import com.nextcloud.model.WorkerStateLiveData import com.nextcloud.utils.ForegroundServiceHelper +import com.nextcloud.utils.extensions.getPercent import com.owncloud.android.R import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.ForegroundServiceType @@ -36,11 +38,13 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCo import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.DownloadFileOperation import com.owncloud.android.operations.DownloadType +import com.owncloud.android.ui.events.EventBusFactory +import com.owncloud.android.ui.events.FileDownloadProgressEvent import com.owncloud.android.utils.theme.ViewThemeUtils -import java.security.SecureRandom import java.util.AbstractList import java.util.Optional import java.util.Vector +import kotlin.random.Random @Suppress("LongParameterList", "TooManyFunctions") class FileDownloadWorker( @@ -49,7 +53,9 @@ class FileDownloadWorker( private var localBroadcastManager: LocalBroadcastManager, private val context: Context, params: WorkerParameters -) : Worker(context, params), OnAccountsUpdateListener, OnDatatransferProgressListener { +) : CoroutineWorker(context, params), + OnAccountsUpdateListener, + OnDatatransferProgressListener { companion object { private val TAG = FileDownloadWorker::class.java.simpleName @@ -62,8 +68,8 @@ class FileDownloadWorker( } } - fun isDownloading(accountName: String, fileId: Long): Boolean { - return pendingDownloads.all.any { it.value?.payload?.isMatching(accountName, fileId) == true } + fun isDownloading(accountName: String, fileId: Long): Boolean = pendingDownloads.all.any { + it.value?.payload?.isMatching(accountName, fileId) == true } const val FILE_REMOTE_PATH = "FILE_REMOTE_PATH" @@ -73,19 +79,14 @@ class FileDownloadWorker( const val ACTIVITY_NAME = "ACTIVITY_NAME" const val PACKAGE_NAME = "PACKAGE_NAME" const val CONFLICT_UPLOAD_ID = "CONFLICT_UPLOAD_ID" - const val EXTRA_DOWNLOAD_RESULT = "EXTRA_DOWNLOAD_RESULT" const val EXTRA_REMOTE_PATH = "EXTRA_REMOTE_PATH" const val EXTRA_LINKED_TO_PATH = "EXTRA_LINKED_TO_PATH" const val EXTRA_ACCOUNT_NAME = "EXTRA_ACCOUNT_NAME" - fun getDownloadAddedMessage(): String { - return FileDownloadWorker::class.java.name + "DOWNLOAD_ADDED" - } + fun getDownloadAddedMessage(): String = FileDownloadWorker::class.java.name + "DOWNLOAD_ADDED" - fun getDownloadFinishMessage(): String { - return FileDownloadWorker::class.java.name + "DOWNLOAD_FINISH" - } + fun getDownloadFinishMessage(): String = FileDownloadWorker::class.java.name + "DOWNLOAD_FINISH" } private var currentDownload: DownloadFileOperation? = null @@ -95,7 +96,7 @@ class FileDownloadWorker( private val intents = FileDownloadIntents(context) private var notificationManager = DownloadNotificationManager( - SecureRandom().nextInt(), + Random.nextInt(), context, viewThemeUtils ) @@ -110,18 +111,17 @@ class FileDownloadWorker( private var downloadError: FileDownloadError? = null - @Suppress("TooGenericExceptionCaught") - override fun doWork(): Result { - return try { - val requestDownloads = getRequestDownloads() - addAccountUpdateListener() + @Suppress("TooGenericExceptionCaught", "ReturnCount") + override suspend fun doWork(): Result { + val foregroundInfo = createWorkerForegroundInfo() + setForeground(foregroundInfo) - val foregroundInfo = ForegroundServiceHelper.createWorkerForegroundInfo( - notificationManager.getId(), - notificationManager.getNotification(), - ForegroundServiceType.DataSync - ) - setForegroundAsync(foregroundInfo) + return try { + setUser() + val remotePath = inputData.keyValueMap[FILE_REMOTE_PATH] as String? ?: return Result.failure() + val ocFile = fileDataStorageManager?.getFileByEncryptedRemotePath(remotePath) ?: return Result.failure() + val requestDownloads = getRequestDownloads(ocFile) + addAccountUpdateListener() requestDownloads.forEach { downloadFile(it) @@ -132,43 +132,39 @@ class FileDownloadWorker( notificationManager.dismissNotification() } - setIdleWorkerState() - Log_OC.e(TAG, "FilesDownloadWorker successfully completed") Result.success() } catch (t: Throwable) { - notificationManager.dismissNotification() notificationManager.showNewNotification(context.getString(R.string.downloader_unexpected_error)) Log_OC.e(TAG, "Error caught at FilesDownloadWorker(): " + t.localizedMessage) - setIdleWorkerState() Result.failure() + } finally { + Log_OC.e(TAG, "FilesDownloadWorker cleanup") + notificationManager.dismissNotification() + setIdleWorkerState() } } - override fun onStopped() { - Log_OC.e(TAG, "FilesDownloadWorker stopped") - - notificationManager.dismissNotification() - setIdleWorkerState() - - super.onStopped() - } + private fun createWorkerForegroundInfo(): ForegroundInfo = ForegroundServiceHelper.createWorkerForegroundInfo( + notificationManager.getId(), + notificationManager.getNotification(), + ForegroundServiceType.DataSync + ) private fun setWorkerState(user: User?) { - WorkerStateLiveData.instance().setWorkState(WorkerState.Download(user, currentDownload)) + WorkerStateLiveData.instance().setWorkState(WorkerState.DownloadStarted(user, currentDownload)) } private fun setIdleWorkerState() { - WorkerStateLiveData.instance().setWorkState(WorkerState.Idle) + WorkerStateLiveData.instance().setWorkState(WorkerState.DownloadFinished(getCurrentFile())) } private fun removePendingDownload(accountName: String?) { pendingDownloads.remove(accountName) } - private fun getRequestDownloads(): AbstractList { - setUser() - val files = getFiles() + private fun getRequestDownloads(ocFile: OCFile): AbstractList { + val files = getFiles(ocFile) val downloadType = getDownloadType() conflictUploadId = inputData.keyValueMap[CONFLICT_UPLOAD_ID] as Long? @@ -221,15 +217,10 @@ class FileDownloadWorker( fileDataStorageManager = FileDataStorageManager(user, context.contentResolver) } - private fun getFiles(): List { - val remotePath = inputData.keyValueMap[FILE_REMOTE_PATH] as String? - val file = fileDataStorageManager?.getFileByEncryptedRemotePath(remotePath) ?: return listOf() - - return if (file.isFolder) { - fileDataStorageManager?.getAllFilesRecursivelyInsideFolder(file) ?: listOf() - } else { - listOf(file) - } + private fun getFiles(file: OCFile): List = if (file.isFolder) { + fileDataStorageManager?.getAllFilesRecursivelyInsideFolder(file) ?: listOf() + } else { + listOf(file) } private fun getDownloadType(): DownloadType? { @@ -267,7 +258,12 @@ class FileDownloadWorker( return } - notifyDownloadStart(currentDownload!!) + lastPercent = 0 + notificationManager.run { + prepareForStart(currentDownload!!) + setContentIntent(intents.detailsIntent(currentDownload!!), PendingIntent.FLAG_IMMUTABLE) + } + var downloadResult: RemoteOperationResult<*>? = null try { val ocAccount = getOCAccountForDownload() @@ -288,15 +284,6 @@ class FileDownloadWorker( } } - private fun notifyDownloadStart(download: DownloadFileOperation) { - lastPercent = 0 - - notificationManager.run { - prepareForStart(download) - setContentIntent(intents.detailsIntent(download), PendingIntent.FLAG_IMMUTABLE) - } - } - @Suppress("DEPRECATION") private fun getOCAccountForDownload(): OwnCloudAccount { val currentDownloadAccount = currentDownload?.user?.toPlatformAccount() @@ -350,6 +337,7 @@ class FileDownloadWorker( private fun checkDownloadError(result: RemoteOperationResult<*>) { if (result.isSuccess || downloadError != null) { + notificationManager.dismissNotification() return } @@ -365,6 +353,7 @@ class FileDownloadWorker( FileDownloadError.Cancelled -> { context.getString(R.string.downloader_file_download_cancelled) } + FileDownloadError.Failed -> { context.getString(R.string.downloader_file_download_failed) } @@ -373,10 +362,7 @@ class FileDownloadWorker( notificationManager.showNewNotification(text) } - private fun notifyDownloadResult( - download: DownloadFileOperation, - downloadResult: RemoteOperationResult<*> - ) { + private fun notifyDownloadResult(download: DownloadFileOperation, downloadResult: RemoteOperationResult<*>) { if (downloadResult.isCancelled) { return } @@ -404,6 +390,10 @@ class FileDownloadWorker( } } + @Suppress("MagicNumber") + private val minProgressUpdateInterval = 750 + private var lastUpdateTime = 0L + @Suppress("MagicNumber") override fun onTransferProgress( progressRate: Long, @@ -411,23 +401,25 @@ class FileDownloadWorker( totalToTransfer: Long, filePath: String ) { - val percent: Int = (100.0 * totalTransferredSoFar.toDouble() / totalToTransfer.toDouble()).toInt() + val percent: Int = downloadProgressListener.getPercent(totalTransferredSoFar, totalToTransfer) + val currentTime = System.currentTimeMillis() - if (percent != lastPercent) { + if (percent != lastPercent && (currentTime - lastUpdateTime) >= minProgressUpdateInterval) { notificationManager.run { - updateDownloadProgress(filePath, percent, totalToTransfer) + updateDownloadProgress(percent, totalToTransfer) } + lastUpdateTime = currentTime } lastPercent = percent + EventBusFactory.downloadProgressEventBus.post(FileDownloadProgressEvent(percent)) } + // CHECK: Is this class still needed after conversion from Foreground Services to Worker? inner class FileDownloadProgressListener : OnDatatransferProgressListener { private val boundListeners: MutableMap = HashMap() - fun isDownloading(user: User?, file: OCFile?): Boolean { - return FileDownloadHelper.instance().isDownloading(user, file) - } + fun isDownloading(user: User?, file: OCFile?): Boolean = FileDownloadHelper.instance().isDownloading(user, file) fun addDataTransferProgressListener(listener: OnDatatransferProgressListener?, file: OCFile?) { if (file == null || listener == null) { diff --git a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt new file mode 100644 index 0000000..1ef2456 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt @@ -0,0 +1,167 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.folderDownload + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.jobs.download.FileDownloadHelper +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.DownloadFileOperation +import com.owncloud.android.operations.DownloadType +import com.owncloud.android.ui.helpers.FileOperationsHelper +import com.owncloud.android.utils.theme.ViewThemeUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.util.concurrent.ConcurrentHashMap + +@Suppress("LongMethod") +class FolderDownloadWorker( + private val accountManager: UserAccountManager, + private val context: Context, + private val viewThemeUtils: ViewThemeUtils, + params: WorkerParameters +) : CoroutineWorker(context, params) { + + companion object { + private const val TAG = "📂" + "FolderDownloadWorker" + const val FOLDER_ID = "FOLDER_ID" + const val ACCOUNT_NAME = "ACCOUNT_NAME" + + private val pendingDownloads: MutableSet = ConcurrentHashMap.newKeySet() + + fun isDownloading(id: Long): Boolean = pendingDownloads.contains(id) + } + + private var notificationManager: FolderDownloadWorkerNotificationManager? = null + private lateinit var storageManager: FileDataStorageManager + + @Suppress("TooGenericExceptionCaught", "ReturnCount", "DEPRECATION") + override suspend fun doWork(): Result { + val folderID = inputData.getLong(FOLDER_ID, -1) + if (folderID == -1L) { + return Result.failure() + } + + val accountName = inputData.getString(ACCOUNT_NAME) + if (accountName == null) { + Log_OC.e(TAG, "failed accountName cannot be null") + return Result.failure() + } + + val optionalUser = accountManager.getUser(accountName) + if (optionalUser.isEmpty) { + Log_OC.e(TAG, "failed user is not present") + return Result.failure() + } + + val user = optionalUser.get() + storageManager = FileDataStorageManager(user, context.contentResolver) + val folder = storageManager.getFileById(folderID) + if (folder == null) { + Log_OC.e(TAG, "failed folder cannot be nul") + return Result.failure() + } + + notificationManager = FolderDownloadWorkerNotificationManager(context, viewThemeUtils) + + Log_OC.d(TAG, "🕒 started for ${user.accountName} downloading ${folder.fileName}") + + val foregroundInfo = notificationManager?.getForegroundInfo(folder) ?: return Result.failure() + setForeground(foregroundInfo) + + pendingDownloads.add(folder.fileId) + + val downloadHelper = FileDownloadHelper.instance() + + return withContext(Dispatchers.IO) { + try { + val files = getFiles(folder, storageManager) + val account = user.toOwnCloudAccount() + val client = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(account, context) + + var result = true + files.forEachIndexed { index, file -> + if (!checkDiskSize(file)) { + return@withContext Result.failure() + } + + withContext(Dispatchers.Main) { + notificationManager?.showProgressNotification( + folder.fileName, + file.fileName, + index, + files.size + ) + } + + val operation = DownloadFileOperation(user, file, context) + val operationResult = operation.execute(client) + if (operationResult?.isSuccess == true && operation.downloadType === DownloadType.DOWNLOAD) { + getOCFile(operation)?.let { ocFile -> + downloadHelper.saveFile(ocFile, operation, storageManager) + } + } + + if (!operationResult.isSuccess) { + result = false + } + } + + withContext(Dispatchers.Main) { + notificationManager?.showCompletionMessage(folder.fileName, result) + } + + if (result) { + Log_OC.d(TAG, "✅ completed") + Result.success() + } else { + Log_OC.d(TAG, "❌ failed") + Result.failure() + } + } catch (e: Exception) { + Log_OC.d(TAG, "❌ failed reason: $e") + Result.failure() + } finally { + pendingDownloads.remove(folder.fileId) + notificationManager?.dismiss() + } + } + } + + private fun getOCFile(operation: DownloadFileOperation): OCFile? { + val file = operation.file?.fileId?.let { storageManager.getFileById(it) } + ?: storageManager.getFileByDecryptedRemotePath(operation.file?.remotePath) + ?: run { + Log_OC.e(TAG, "could not save ${operation.file?.remotePath}") + return null + } + + return file + } + + private fun getFiles(folder: OCFile, storageManager: FileDataStorageManager): List = + storageManager.getFolderContent(folder, false) + .filter { !it.isFolder && !it.isDown } + + private suspend fun checkDiskSize(file: OCFile): Boolean { + val fileSizeInByte = file.fileLength + val availableDiskSpace = FileOperationsHelper.getAvailableSpaceOnDevice() + + return if (availableDiskSpace < fileSizeInByte) { + notificationManager?.showNotAvailableDiskSpace() + false + } else { + true + } + } +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorkerNotificationManager.kt b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorkerNotificationManager.kt new file mode 100644 index 0000000..6b2a1f4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorkerNotificationManager.kt @@ -0,0 +1,113 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.folderDownload + +import android.app.Notification +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.work.ForegroundInfo +import com.nextcloud.client.jobs.notification.WorkerNotificationManager +import com.nextcloud.utils.ForegroundServiceHelper +import com.owncloud.android.R +import com.owncloud.android.datamodel.ForegroundServiceType +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.ui.notifications.NotificationUtils +import com.owncloud.android.utils.theme.ViewThemeUtils +import kotlinx.coroutines.delay +import kotlin.random.Random + +class FolderDownloadWorkerNotificationManager(private val context: Context, viewThemeUtils: ViewThemeUtils) : + WorkerNotificationManager( + id = NOTIFICATION_ID, + context, + viewThemeUtils, + tickerId = R.string.folder_download_worker_ticker_id, + channelId = NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD + ) { + + companion object { + private const val NOTIFICATION_ID = 391 + private const val MAX_PROGRESS = 100 + private const val DELAY = 1000L + } + + private fun getNotification(title: String, description: String? = null, progress: Int? = null): Notification = + notificationBuilder.apply { + setSmallIcon(R.drawable.ic_sync) + setContentTitle(title) + clearActions() + + description?.let { + setContentText(description) + } + + progress?.let { + setProgress(MAX_PROGRESS, progress, false) + addAction( + android.R.drawable.ic_menu_close_clear_cancel, + context.getString(R.string.common_cancel), + getCancelPendingIntent() + ) + } + + setAutoCancel(true) + }.build() + + private fun getCancelPendingIntent(): PendingIntent { + val intent = Intent(context, FolderDownloadWorkerReceiver::class.java) + + return PendingIntent.getBroadcast( + context, + Random.nextInt(), + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } + + fun showProgressNotification(folderName: String, filename: String, currentIndex: Int, totalFileSize: Int) { + val currentFileIndex = (currentIndex + 1) + val description = context.getString(R.string.folder_download_counter, currentFileIndex, totalFileSize, filename) + val progress = (currentFileIndex * MAX_PROGRESS) / totalFileSize + val notification = getNotification(title = folderName, description = description, progress = progress) + notificationManager.notify(NOTIFICATION_ID, notification) + } + + suspend fun showCompletionMessage(folderName: String, success: Boolean) { + val title = if (success) { + context.getString(R.string.folder_download_success_notification_title, folderName) + } else { + context.getString(R.string.folder_download_error_notification_title, folderName) + } + + val notification = getNotification(title = title) + notificationManager.notify(NOTIFICATION_ID, notification) + + delay(DELAY) + dismiss() + } + + fun getForegroundInfo(folder: OCFile): ForegroundInfo = ForegroundServiceHelper.createWorkerForegroundInfo( + NOTIFICATION_ID, + getNotification(folder.fileName, progress = 0), + ForegroundServiceType.DataSync + ) + + suspend fun showNotAvailableDiskSpace() { + val title = context.getString(R.string.folder_download_insufficient_disk_space_notification_title) + val notification = getNotification(title) + notificationManager.notify(NOTIFICATION_ID, notification) + + delay(DELAY) + dismiss() + } + + fun dismiss() { + notificationManager.cancel(NOTIFICATION_ID) + } +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorkerReceiver.kt b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorkerReceiver.kt new file mode 100644 index 0000000..399c1cc --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorkerReceiver.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.folderDownload + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.nextcloud.client.jobs.BackgroundJobManager +import com.owncloud.android.MainApp +import javax.inject.Inject + +class FolderDownloadWorkerReceiver : BroadcastReceiver() { + @Inject + lateinit var backgroundJobManager: BackgroundJobManager + + override fun onReceive(context: Context, intent: Intent) { + MainApp.getAppComponent().inject(this) + backgroundJobManager.cancelFolderDownload() + } +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/metadata/MetadataWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/metadata/MetadataWorker.kt new file mode 100644 index 0000000..b6e19b3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/metadata/MetadataWorker.kt @@ -0,0 +1,83 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.metadata + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.nextcloud.client.account.User +import com.nextcloud.utils.extensions.getNonEncryptedSubfolders +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 kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class MetadataWorker(private val context: Context, params: WorkerParameters, private val user: User) : + CoroutineWorker(context, params) { + + companion object { + private const val TAG = "MetadataWorker" + const val FILE_PATH = "file_path" + } + + @Suppress("DEPRECATION", "ReturnCount") + override suspend fun doWork(): Result { + val storageManager = FileDataStorageManager(user, context.contentResolver) + val filePath = inputData.getString(FILE_PATH) + if (filePath == null) { + Log_OC.e(TAG, "❌ Invalid folder path. Aborting metadata sync. $filePath") + return Result.failure() + } + val currentDir = storageManager.getFileByDecryptedRemotePath(filePath) + if (currentDir == null) { + Log_OC.e(TAG, "❌ Current directory is null. Aborting metadata sync. $filePath") + return Result.failure() + } + Log_OC.d(TAG, "🕒 Starting metadata sync for folder: $filePath") + + // first check current dir + refreshFolder(currentDir, storageManager) + + // then get up-to-date subfolders + val subfolders = storageManager.getNonEncryptedSubfolders(currentDir.fileId, user.accountName) + subfolders.forEach { subFolder -> + refreshFolder(subFolder, storageManager) + } + + Log_OC.d(TAG, "🏁 Metadata sync completed for folder: $filePath") + return Result.success() + } + + @Suppress("DEPRECATION") + private suspend fun refreshFolder(folder: OCFile, storageManager: FileDataStorageManager) = + withContext(Dispatchers.IO) { + Log_OC.d( + TAG, + "📂 eTag check\n" + + " Path: " + folder.remotePath + "\n" + + " eTag: " + folder.etag + "\n" + + " eTagOnServer: " + folder.etagOnServer + ) + if (!folder.isEtagChanged) { + Log_OC.d(TAG, "Skipping ${folder.remotePath}, eTag didn't change") + return@withContext + } + + Log_OC.d(TAG, "⏳ Fetching metadata for: ${folder.remotePath}") + + val operation = RefreshFolderOperation(folder, storageManager, user, context) + val result = operation.execute(user, context) + if (result.isSuccess) { + Log_OC.d(TAG, "✅ Successfully fetched metadata for: ${folder.remotePath}") + } else { + Log_OC.e(TAG, "❌ Failed to fetch metadata for: ${folder.remotePath}") + } + } +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/notification/WorkerNotificationManager.kt b/app/src/main/java/com/nextcloud/client/jobs/notification/WorkerNotificationManager.kt new file mode 100644 index 0000000..e427142 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/notification/WorkerNotificationManager.kt @@ -0,0 +1,70 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.notification + +import android.app.Notification +import android.app.NotificationManager +import android.content.Context +import android.graphics.BitmapFactory +import android.os.Handler +import android.os.Looper +import androidx.core.app.NotificationCompat +import com.owncloud.android.R +import com.owncloud.android.utils.theme.ViewThemeUtils + +open class WorkerNotificationManager( + private val id: Int, + private val context: Context, + viewThemeUtils: ViewThemeUtils, + private val tickerId: Int, + channelId: String +) { + var currentOperationTitle: String? = null + + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + var notificationBuilder: NotificationCompat.Builder = + NotificationCompat.Builder(context, channelId).apply { + setTicker(context.getString(tickerId)) + setSmallIcon(R.drawable.notification_icon) + setLargeIcon(BitmapFactory.decodeResource(context.resources, R.drawable.notification_icon)) + setStyle(NotificationCompat.BigTextStyle()) + priority = NotificationCompat.PRIORITY_LOW + setSound(null) + setVibrate(null) + setOnlyAlertOnce(true) + setSilent(true) + viewThemeUtils.androidx.themeNotificationCompatBuilder(context, this) + } + + fun showNotification() { + notificationManager.notify(id, notificationBuilder.build()) + } + + @Suppress("MagicNumber") + fun setProgress(percent: Int, progressText: String?, indeterminate: Boolean) { + notificationBuilder.run { + setProgress(100, percent, indeterminate) + setContentTitle(currentOperationTitle) + + progressText?.let { + setContentText(progressText) + } + } + } + + fun dismissNotification(delay: Long = 0) { + Handler(Looper.getMainLooper()).postDelayed({ + notificationManager.cancel(id) + }, delay) + } + + fun getId(): Int = id + + fun getNotification(): Notification = notificationBuilder.build() +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsNotificationManager.kt b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsNotificationManager.kt new file mode 100644 index 0000000..fce69c3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsNotificationManager.kt @@ -0,0 +1,168 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.offlineOperations + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.core.app.NotificationCompat +import com.nextcloud.client.database.entity.OfflineOperationEntity +import com.nextcloud.client.jobs.notification.WorkerNotificationManager +import com.nextcloud.client.jobs.offlineOperations.receiver.OfflineOperationReceiver +import com.nextcloud.utils.extensions.getErrorMessage +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.ui.activity.ConflictsResolveActivity +import com.owncloud.android.ui.notifications.NotificationUtils +import com.owncloud.android.utils.theme.ViewThemeUtils + +class OfflineOperationsNotificationManager(private val context: Context, viewThemeUtils: ViewThemeUtils) : + WorkerNotificationManager( + ID, + context, + viewThemeUtils, + tickerId = R.string.offline_operations_worker_notification_manager_ticker, + channelId = NotificationUtils.NOTIFICATION_CHANNEL_OFFLINE_OPERATIONS + ) { + + companion object { + private const val ID = 121 + const val ERROR_ID = 122 + + private const val ONE_HUNDRED_PERCENT = 100 + } + + fun start() { + notificationBuilder.run { + setContentTitle(context.getString(R.string.offline_operations_worker_notification_start_text)) + setProgress(ONE_HUNDRED_PERCENT, 0, false) + } + + showNotification() + } + + fun update(totalOperationSize: Int, currentOperationIndex: Int, filename: String) { + val title = if (totalOperationSize > 1) { + String.format( + context.getString(R.string.offline_operations_worker_progress_text), + currentOperationIndex, + totalOperationSize, + filename + ) + } else { + filename + } + + val progress = (currentOperationIndex * ONE_HUNDRED_PERCENT) / totalOperationSize + + notificationBuilder.run { + setContentTitle(title) + setProgress(ONE_HUNDRED_PERCENT, progress, false) + } + + showNotification() + } + + fun showNewNotification(id: Int?, result: RemoteOperationResult<*>, operation: RemoteOperation<*>) { + val reason = (result to operation).getErrorMessage() + val text = context.getString(R.string.offline_operations_worker_notification_error_text, reason) + val cancelOfflineOperationAction = id?.let { getCancelOfflineOperationAction(it) } + + notificationBuilder.run { + cancelOfflineOperationAction?.let { + addAction(it) + } + setContentTitle(text) + setOngoing(false) + setProgress(0, 0, false) + notificationManager.notify(ERROR_ID, this.build()) + } + } + + fun showConflictNotificationForDeleteOrRemoveOperation(entity: OfflineOperationEntity?) { + val id = entity?.id + if (id == null) { + return + } + + val title = entity.getConflictText(context) + + notificationBuilder + .setProgress(0, 0, false) + .setOngoing(false) + .clearActions() + .setContentTitle(title) + + notificationManager.notify(id, notificationBuilder.build()) + } + + fun showConflictResolveNotification(file: OCFile, entity: OfflineOperationEntity?) { + val path = entity?.path + val id = entity?.id + + if (path == null || id == null) { + return + } + + val resolveConflictAction = getResolveConflictAction(file, id, path) + + val title = entity.getConflictText(context) + + notificationBuilder + .setProgress(0, 0, false) + .setOngoing(false) + .clearActions() + .setContentTitle(title) + .setContentIntent(resolveConflictAction.actionIntent) + .addAction(resolveConflictAction) + + notificationManager.notify(id, notificationBuilder.build()) + } + + private fun getResolveConflictAction(file: OCFile, id: Int, path: String): NotificationCompat.Action { + val intent = ConflictsResolveActivity.createIntent(file, path, context) + val pendingIntent = PendingIntent.getActivity( + context, + id, + intent, + PendingIntent.FLAG_IMMUTABLE + ) + + return NotificationCompat.Action( + R.drawable.ic_cloud_upload, + context.getString(R.string.upload_list_resolve_conflict), + pendingIntent + ) + } + + private fun getCancelOfflineOperationAction(id: Int): NotificationCompat.Action { + val intent = Intent(context, OfflineOperationReceiver::class.java).apply { + putExtra(OfflineOperationReceiver.ID, id) + } + + val pendingIntent = PendingIntent.getBroadcast( + context, + id, + intent, + PendingIntent.FLAG_IMMUTABLE + ) + + return NotificationCompat.Action( + R.drawable.ic_delete, + context.getString(R.string.common_cancel), + pendingIntent + ) + } + + fun dismissNotification(id: Int?) { + if (id == null) return + notificationManager.cancel(id) + } +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt new file mode 100644 index 0000000..9094918 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt @@ -0,0 +1,301 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.offlineOperations + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.nextcloud.client.account.User +import com.nextcloud.client.database.entity.OfflineOperationEntity +import com.nextcloud.client.jobs.offlineOperations.repository.OfflineOperationsRepository +import com.nextcloud.client.network.ClientFactoryImpl +import com.nextcloud.client.network.ConnectivityService +import com.nextcloud.model.OfflineOperationType +import com.nextcloud.model.WorkerState +import com.nextcloud.model.WorkerStateLiveData +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation +import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation +import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation +import com.owncloud.android.lib.resources.files.model.RemoteFile +import com.owncloud.android.operations.CreateFolderOperation +import com.owncloud.android.operations.RemoveFileOperation +import com.owncloud.android.operations.RenameFileOperation +import com.owncloud.android.utils.MimeTypeUtil +import com.owncloud.android.utils.theme.ViewThemeUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +private typealias OfflineOperationResult = Pair?, RemoteOperation<*>?>? + +class OfflineOperationsWorker( + private val user: User, + private val context: Context, + private val connectivityService: ConnectivityService, + viewThemeUtils: ViewThemeUtils, + params: WorkerParameters +) : CoroutineWorker(context, params) { + + companion object { + private val TAG = OfflineOperationsWorker::class.java.simpleName + const val JOB_NAME = "JOB_NAME" + + private const val ONE_SECOND = 1000L + } + + private val fileDataStorageManager = FileDataStorageManager(user, context.contentResolver) + private val clientFactory = ClientFactoryImpl(context) + private val notificationManager = OfflineOperationsNotificationManager(context, viewThemeUtils) + private var repository = OfflineOperationsRepository(fileDataStorageManager) + + @Suppress("TooGenericExceptionCaught") + override suspend fun doWork(): Result = withContext(Dispatchers.IO) { + try { + val jobName = inputData.getString(JOB_NAME) + Log_OC.d(TAG, "[$jobName] OfflineOperationsWorker started for user: ${user.accountName}") + + // check network connection + if (!isNetworkAndServerAvailable()) { + Log_OC.w(TAG, "⚠️ No internet/server connection. Retrying later...") + return@withContext Result.retry() + } + + // check offline operations + val operations = fileDataStorageManager.offlineOperationDao.getAll() + if (operations.isEmpty()) { + Log_OC.d(TAG, "Skipping, no offline operation found") + return@withContext Result.success() + } + + // process offline operations + notificationManager.start() + val client = clientFactory.create(user) + processOperations(operations, client) + + // finish + WorkerStateLiveData.instance().setWorkState(WorkerState.OfflineOperationsCompleted) + Log_OC.d(TAG, "🏁 Worker finished with result") + return@withContext Result.success() + } catch (e: Exception) { + Log_OC.e(TAG, "💥 ProcessOperations failed: ${e.message}") + return@withContext Result.failure() + } finally { + notificationManager.dismissNotification() + } + } + + // region Handle offline operations + @Suppress("TooGenericExceptionCaught") + private suspend fun processOperations(operations: List, client: OwnCloudClient) { + val totalOperationSize = operations.size + operations.forEachIndexed { index, operation -> + try { + Log_OC.d(TAG, "Processing operation, path: ${operation.path}") + val result = executeOperation(operation, client) + handleResult(operation, totalOperationSize, index, result) + } catch (e: Exception) { + Log_OC.e(TAG, "💥 Exception while processing operation id=${operation.id}: ${e.message}") + } + } + } + + private fun handleResult( + operation: OfflineOperationEntity, + totalOperations: Int, + currentSuccessfulOperationIndex: Int, + result: OfflineOperationResult + ) { + val operationResult = result?.first ?: return + val logMessage = if (operationResult.isSuccess) "Operation completed" else "Operation failed" + Log_OC.d(TAG, "$logMessage filename: ${operation.filename}, type: ${operation.type}") + + return if (result.first?.isSuccess == true) { + handleSuccessResult(operation, totalOperations, currentSuccessfulOperationIndex) + } else { + handleErrorResult(operation.id, result) + } + } + + private fun handleSuccessResult( + operation: OfflineOperationEntity, + totalOperations: Int, + currentSuccessfulOperationIndex: Int + ) { + if (operation.type is OfflineOperationType.RemoveFile) { + val operationType = operation.type as OfflineOperationType.RemoveFile + fileDataStorageManager.getFileByDecryptedRemotePath(operationType.path)?.let { ocFile -> + repository.deleteOperation(ocFile) + } + } else { + repository.updateNextOperations(operation) + } + + fileDataStorageManager.offlineOperationDao.delete(operation) + notificationManager.update(totalOperations, currentSuccessfulOperationIndex + 1, operation.filename ?: "") + } + + private fun handleErrorResult(id: Int?, result: OfflineOperationResult) { + val operationResult = result?.first ?: return + val operation = result.second ?: return + Log_OC.e(TAG, "❌ Operation failed [id=$id]: code=${operationResult.code}, message=${operationResult.message}") + val excludedErrorCodes = + listOf(RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS, RemoteOperationResult.ResultCode.LOCKED) + + if (!excludedErrorCodes.contains(operationResult.code)) { + notificationManager.showNewNotification(id, operationResult, operation) + } else { + Log_OC.d(TAG, "ℹ️ Ignored error: ${operationResult.code}") + } + } + // endregion + + private suspend fun isNetworkAndServerAvailable(): Boolean = suspendCoroutine { continuation -> + connectivityService.isNetworkAndServerAvailable { result -> + continuation.resume(result) + } + } + + // region Operation Execution + @Suppress("ComplexCondition", "LongMethod") + private suspend fun executeOperation( + operation: OfflineOperationEntity, + client: OwnCloudClient + ): OfflineOperationResult? = withContext(Dispatchers.IO) { + var path = (operation.path) + if (path == null) { + Log_OC.w(TAG, "⚠️ Skipped: path is null for operation id=${operation.id}") + return@withContext null + } + + if (operation.type is OfflineOperationType.CreateFile && path.endsWith(OCFile.PATH_SEPARATOR)) { + Log_OC.w( + TAG, + "Create file operation should not ends with path separator removing suffix, " + + "operation id=${operation.id}" + ) + path = path.removeSuffix(OCFile.PATH_SEPARATOR) + } + + val remoteFile = getRemoteFile(path) + val ocFile = fileDataStorageManager.getFileByDecryptedRemotePath(path) + + if (remoteFile != null && ocFile != null && isFileChanged(remoteFile, ocFile)) { + Log_OC.w(TAG, "⚠️ Conflict detected: File already exists on server. Skipping operation id=${operation.id}") + + if (operation.isRenameOrRemove()) { + Log_OC.d(TAG, "🗑 Removing conflicting rename/remove operation id=${operation.id}") + fileDataStorageManager.offlineOperationDao.delete(operation) + notificationManager.showConflictNotificationForDeleteOrRemoveOperation(operation) + } else { + Log_OC.d(TAG, "📌 Showing conflict resolution for operation id=${operation.id}") + notificationManager.showConflictResolveNotification(ocFile, operation) + } + + return@withContext null + } + + if (operation.isRenameOrRemove() && ocFile == null) { + Log_OC.d(TAG, "Skipping, attempting to delete or rename non-existing file") + fileDataStorageManager.offlineOperationDao.delete(operation) + return@withContext null + } + + if (operation.isCreate() && remoteFile != null && ocFile != null && !isFileChanged(remoteFile, ocFile)) { + Log_OC.d(TAG, "Skipping, attempting to create same file creation") + fileDataStorageManager.offlineOperationDao.delete(operation) + return@withContext null + } + + return@withContext when (val type = operation.type) { + is OfflineOperationType.CreateFolder -> { + Log_OC.d(TAG, "📂 Creating folder at ${type.path}") + createFolder(operation, client) + } + is OfflineOperationType.CreateFile -> { + Log_OC.d(TAG, "📤 Uploading file: local=${type.localPath} → remote=${type.remotePath}") + createFile(operation, client) + } + is OfflineOperationType.RenameFile -> { + Log_OC.d(TAG, "✏️ Renaming ${operation.path} → ${type.newName}") + renameFile(operation, client) + } + is OfflineOperationType.RemoveFile -> { + Log_OC.d(TAG, "🗑 Removing file: ${operation.path}") + ocFile?.let { removeFile(it, client) } + } + else -> { + Log_OC.d(TAG, "⚠️ Unsupported operation type: $type") + null + } + } + } + + @Suppress("DEPRECATION") + private fun createFolder(operation: OfflineOperationEntity, client: OwnCloudClient): OfflineOperationResult { + val operationType = (operation.type as OfflineOperationType.CreateFolder) + val createFolderOperation = CreateFolderOperation(operationType.path, user, context, fileDataStorageManager) + return createFolderOperation.execute(client) to createFolderOperation + } + + @Suppress("DEPRECATION") + private fun createFile(operation: OfflineOperationEntity, client: OwnCloudClient): OfflineOperationResult { + val operationType = (operation.type as OfflineOperationType.CreateFile) + val lastModificationDate = System.currentTimeMillis() / ONE_SECOND + val createFileOperation = UploadFileRemoteOperation( + operationType.localPath, + operationType.remotePath, + operationType.mimeType, + "", + operation.modifiedAt ?: lastModificationDate, + operation.createdAt ?: System.currentTimeMillis(), + true + ) + return createFileOperation.execute(client) to createFileOperation + } + + @Suppress("DEPRECATION") + private fun renameFile(operation: OfflineOperationEntity, client: OwnCloudClient): OfflineOperationResult { + val operationType = (operation.type as OfflineOperationType.RenameFile) + val renameFileOperation = RenameFileOperation(operation.path, operationType.newName, fileDataStorageManager) + return renameFileOperation.execute(client) to renameFileOperation + } + + @Suppress("DEPRECATION") + private fun removeFile(ocFile: OCFile, client: OwnCloudClient): OfflineOperationResult { + val removeFileOperation = RemoveFileOperation(ocFile, false, user, true, context, fileDataStorageManager) + return removeFileOperation.execute(client) to removeFileOperation + } + // endregion + + @Suppress("DEPRECATION") + private fun getRemoteFile(remotePath: String): RemoteFile? { + val mimeType = MimeTypeUtil.getMimeTypeFromPath(remotePath) + val isFolder = MimeTypeUtil.isFolder(mimeType) + val client = ClientFactoryImpl(context).create(user) + val result = if (isFolder) { + ReadFolderRemoteOperation(remotePath).execute(client) + } else { + ReadFileRemoteOperation(remotePath).execute(client) + } + + return if (result.isSuccess) { + result.data[0] as? RemoteFile + } else { + null + } + } + + private fun isFileChanged(remoteFile: RemoteFile, ocFile: OCFile): Boolean = remoteFile.etag != ocFile.etagOnServer +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/receiver/OfflineOperationReceiver.kt b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/receiver/OfflineOperationReceiver.kt new file mode 100644 index 0000000..54179ed --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/receiver/OfflineOperationReceiver.kt @@ -0,0 +1,41 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.offlineOperations.receiver + +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsNotificationManager +import com.owncloud.android.MainApp +import com.owncloud.android.datamodel.FileDataStorageManager +import javax.inject.Inject + +class OfflineOperationReceiver : BroadcastReceiver() { + companion object { + const val ID = "id" + } + + @Inject + lateinit var storageManager: FileDataStorageManager + + override fun onReceive(context: Context, intent: Intent) { + MainApp.getAppComponent().inject(this) + + val id = intent.getIntExtra(ID, -1) + if (id == -1) { + return + } + + storageManager.offlineOperationDao.deleteById(id) + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel( + OfflineOperationsNotificationManager.ERROR_ID + ) + } +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt new file mode 100644 index 0000000..86170e4 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepository.kt @@ -0,0 +1,114 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.offlineOperations.repository + +import com.nextcloud.client.database.entity.OfflineOperationEntity +import com.nextcloud.model.OfflineOperationType +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.utils.MimeType +import com.owncloud.android.utils.MimeTypeUtil + +class OfflineOperationsRepository(private val fileDataStorageManager: FileDataStorageManager) : + OfflineOperationsRepositoryType { + + private val dao = fileDataStorageManager.offlineOperationDao + private val pathSeparator = '/' + + @Suppress("NestedBlockDepth") + override fun getAllSubEntities(fileId: Long): List { + val result = mutableListOf() + val queue = ArrayDeque() + queue.add(fileId) + val processedIds = mutableSetOf() + + while (queue.isNotEmpty()) { + val currentFileId = queue.removeFirst() + if (currentFileId in processedIds || currentFileId == 1L) continue + + processedIds.add(currentFileId) + + val subDirectories = dao.getSubEntitiesByParentOCFileId(currentFileId) + result.addAll(subDirectories) + + subDirectories.forEach { + val ocFile = fileDataStorageManager.getFileByDecryptedRemotePath(it.path) + ocFile?.fileId?.let { newFileId -> + if (newFileId != 1L && newFileId !in processedIds) { + queue.add(newFileId) + } + } + } + } + + return result + } + + override fun deleteOperation(file: OCFile) { + if (file.isFolder) { + getAllSubEntities(file.fileId).forEach { + dao.delete(it) + } + } + + file.decryptedRemotePath?.let { + dao.deleteByPath(it) + } + + fileDataStorageManager.removeFile(file, true, true) + } + + override fun updateNextOperations(operation: OfflineOperationEntity) { + val ocFile = fileDataStorageManager.getFileByDecryptedRemotePath(operation.path) + val fileId = ocFile?.fileId ?: return + + getAllSubEntities(fileId) + .mapNotNull { nextOperation -> + nextOperation.parentOCFileId?.let { parentId -> + fileDataStorageManager.getFileById(parentId)?.let { ocFile -> + ocFile.decryptedRemotePath?.let { updatedPath -> + val newPath = updatedPath + nextOperation.filename + pathSeparator + + if (newPath != nextOperation.path) { + nextOperation.apply { + type = when (type) { + is OfflineOperationType.CreateFile -> + (type as OfflineOperationType.CreateFile).copy( + remotePath = newPath + ) + + is OfflineOperationType.CreateFolder -> + (type as OfflineOperationType.CreateFolder).copy( + path = newPath + ) + + else -> type + } + path = newPath + } + } else { + null + } + } + } + } + } + .forEach { dao.update(it) } + } + + override fun convertToOCFiles(fileId: Long): List = + dao.getSubEntitiesByParentOCFileId(fileId).map { entity -> + OCFile(entity.path).apply { + mimeType = if (entity.type is OfflineOperationType.CreateFolder) { + MimeType.DIRECTORY + } else { + MimeTypeUtil.getMimeTypeFromPath(entity.path) + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepositoryType.kt b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepositoryType.kt new file mode 100644 index 0000000..b650909 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/repository/OfflineOperationsRepositoryType.kt @@ -0,0 +1,18 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.offlineOperations.repository + +import com.nextcloud.client.database.entity.OfflineOperationEntity +import com.owncloud.android.datamodel.OCFile + +interface OfflineOperationsRepositoryType { + fun getAllSubEntities(fileId: Long): List + fun deleteOperation(file: OCFile) + fun updateNextOperations(operation: OfflineOperationEntity) + fun convertToOCFiles(fileId: Long): List +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt new file mode 100644 index 0000000..6cbcb77 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/operation/FileOperationHelper.kt @@ -0,0 +1,66 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.operation + +import android.content.Context +import com.nextcloud.client.account.User +import com.nextcloud.utils.extensions.getErrorMessage +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.RemoveFileOperation +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.withContext + +class FileOperationHelper( + private val user: User, + private val context: Context, + private val fileDataStorageManager: FileDataStorageManager +) { + companion object { + private val TAG = FileOperationHelper::class.java.simpleName + } + + @Suppress("TooGenericExceptionCaught", "Deprecation") + suspend fun removeFile( + file: OCFile, + onlyLocalCopy: Boolean, + inBackground: Boolean, + client: OwnCloudClient + ): Boolean { + return withContext(Dispatchers.IO) { + try { + val operation = async { + RemoveFileOperation( + file, + onlyLocalCopy, + user, + inBackground, + context, + fileDataStorageManager + ) + } + val operationResult = operation.await() + val result = operationResult.execute(client) + + return@withContext if (result.isSuccess) { + true + } else { + val reason = (result to operationResult).getErrorMessage() + Log_OC.e(TAG, "Error occurred while removing file: $reason") + false + } + } catch (e: Exception) { + Log_OC.e(TAG, "Error occurred while removing file: $e") + false + } + } + } +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/transfer/FileTransferService.kt b/app/src/main/java/com/nextcloud/client/jobs/transfer/FileTransferService.kt index 971a76e..cfe0d0d 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/transfer/FileTransferService.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/transfer/FileTransferService.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs.transfer @@ -41,27 +41,23 @@ class FileTransferService : LifecycleService() { const val EXTRA_REQUEST = "request" const val EXTRA_USER = "user" - fun createBindIntent(context: Context, user: User): Intent { - return Intent(context, FileTransferService::class.java).apply { + fun createBindIntent(context: Context, user: User): Intent = + Intent(context, FileTransferService::class.java).apply { putExtra(EXTRA_USER, user) } - } - fun createTransferRequestIntent(context: Context, request: Request): Intent { - return Intent(context, FileTransferService::class.java).apply { + fun createTransferRequestIntent(context: Context, request: Request): Intent = + Intent(context, FileTransferService::class.java).apply { action = ACTION_TRANSFER putExtra(EXTRA_REQUEST, request) } - } } /** * Binder forwards [TransferManager] API calls to selected instance of downloader. */ - class Binder( - downloader: TransferManagerImpl, - service: FileTransferService - ) : LocalBinder(service), + class Binder(downloader: TransferManagerImpl, service: FileTransferService) : + LocalBinder(service), TransferManager by downloader @Inject diff --git a/app/src/main/java/com/nextcloud/client/jobs/transfer/Transfer.kt b/app/src/main/java/com/nextcloud/client/jobs/transfer/Transfer.kt index 8a3fa28..b3ecdae 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/transfer/Transfer.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/transfer/Transfer.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs.transfer diff --git a/app/src/main/java/com/nextcloud/client/jobs/transfer/TransferManager.kt b/app/src/main/java/com/nextcloud/client/jobs/transfer/TransferManager.kt index d728fda..ccb688d 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/transfer/TransferManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/transfer/TransferManager.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs.transfer @@ -19,11 +19,7 @@ interface TransferManager { /** * Snapshot of transfer manager status. All data is immutable and can be safely shared. */ - data class Status( - val pending: List, - val running: List, - val completed: List - ) { + data class Status(val pending: List, val running: List, val completed: List) { companion object { val EMPTY = Status(emptyList(), emptyList(), emptyList()) } diff --git a/app/src/main/java/com/nextcloud/client/jobs/transfer/TransferManagerConnection.kt b/app/src/main/java/com/nextcloud/client/jobs/transfer/TransferManagerConnection.kt index 34bc9af..d5d97c3 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/transfer/TransferManagerConnection.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/transfer/TransferManagerConnection.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs.transfer @@ -16,10 +16,9 @@ import com.nextcloud.client.files.Request import com.owncloud.android.datamodel.OCFile import java.util.UUID -class TransferManagerConnection( - context: Context, - val user: User -) : LocalConnection(context), TransferManager { +class TransferManagerConnection(context: Context, val user: User) : + LocalConnection(context), + TransferManager { private var transferListeners: MutableSet<(Transfer) -> Unit> = mutableSetOf() private var statusListeners: MutableSet<(TransferManager.Status) -> Unit> = mutableSetOf() @@ -64,9 +63,7 @@ class TransferManagerConnection( binder?.removeStatusListener(listener) } - override fun createBindIntent(): Intent { - return FileTransferService.createBindIntent(context, user) - } + override fun createBindIntent(): Intent = FileTransferService.createBindIntent(context, user) override fun onBound(binder: IBinder) { super.onBound(binder) diff --git a/app/src/main/java/com/nextcloud/client/jobs/transfer/TransferManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/transfer/TransferManagerImpl.kt index 44defb1..45008fe 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/transfer/TransferManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/transfer/TransferManagerImpl.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs.transfer @@ -118,8 +118,8 @@ class TransferManagerImpl( } } - private fun createDownloadTask(request: DownloadRequest): TaskFunction { - return if (request.test) { + private fun createDownloadTask(request: DownloadRequest): TaskFunction = + if (request.test) { { progress: OnProgressCallback, isCancelled: IsCancelled -> testDownloadTask(request.file, progress, isCancelled) } @@ -130,25 +130,22 @@ class TransferManagerImpl( } wrapper } - } - private fun createUploadTask(request: UploadRequest): TaskFunction { - return if (request.test) { - { progress: OnProgressCallback, isCancelled: IsCancelled -> - val file = UploadFileOperation.obtainNewOCFileToUpload( - request.upload.remotePath, - request.upload.localPath, - request.upload.mimeType - ) - testUploadTask(file, progress, isCancelled) - } - } else { - val uploadTask = uploadTaskFactory.create() - val wrapper: TaskFunction = { _: ((Int) -> Unit), _ -> - uploadTask.upload(request.user, request.upload) - } - wrapper + private fun createUploadTask(request: UploadRequest): TaskFunction = if (request.test) { + { progress: OnProgressCallback, isCancelled: IsCancelled -> + val file = UploadFileOperation.obtainNewOCFileToUpload( + request.upload.remotePath, + request.upload.localPath, + request.upload.mimeType + ) + testUploadTask(file, progress, isCancelled) } + } else { + val uploadTask = uploadTaskFactory.create() + val wrapper: TaskFunction = { _: ((Int) -> Unit), _ -> + uploadTask.upload(request.user, request.upload) + } + wrapper } private fun onTransferUpdate(transfer: Transfer) { diff --git a/app/src/main/java/com/nextcloud/client/jobs/transfer/TransferState.kt b/app/src/main/java/com/nextcloud/client/jobs/transfer/TransferState.kt index 596e51a..5f6b39c 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/transfer/TransferState.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/transfer/TransferState.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs.transfer diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadBroadcastReceiver.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadBroadcastReceiver.kt new file mode 100644 index 0000000..eca87bf --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadBroadcastReceiver.kt @@ -0,0 +1,48 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.upload + +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.owncloud.android.MainApp +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.ui.notifications.NotificationUtils +import javax.inject.Inject + +class FileUploadBroadcastReceiver : BroadcastReceiver() { + + @Inject + lateinit var uploadsStorageManager: UploadsStorageManager + + companion object { + const val UPLOAD_ID = "UPLOAD_ID" + const val REMOTE_PATH = "REMOTE_PATH" + const val STORAGE_PATH = "STORAGE_PATH" + } + + @Suppress("ReturnCount") + override fun onReceive(context: Context, intent: Intent) { + MainApp.getAppComponent().inject(this) + + val remotePath = intent.getStringExtra(REMOTE_PATH) ?: return + val storagePath = intent.getStringExtra(STORAGE_PATH) ?: return + val uploadId = intent.getLongExtra(UPLOAD_ID, -1L) + if (uploadId == -1L) { + return + } + + uploadsStorageManager.removeUpload(uploadId) + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel( + NotificationUtils.createUploadNotificationTag(remotePath, storagePath), + FileUploadWorker.NOTIFICATION_ERROR_ID + ) + } +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt index fc31abb..4b3568a 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt @@ -1,36 +1,51 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs.upload +import android.app.Activity import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.database.entity.toOCUpload +import com.nextcloud.client.database.entity.toUploadEntity +import com.nextcloud.client.device.BatteryStatus import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.jobs.BackgroundJobManager import com.nextcloud.client.jobs.upload.FileUploadWorker.Companion.currentUploadFileOperation +import com.nextcloud.client.network.Connectivity import com.nextcloud.client.network.ConnectivityService +import com.nextcloud.utils.extensions.getUploadIds import com.owncloud.android.MainApp +import com.owncloud.android.R +import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.datamodel.UploadsStorageManager.UploadStatus import com.owncloud.android.db.OCUpload import com.owncloud.android.db.UploadResult import com.owncloud.android.files.services.NameCollisionPolicy +import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.network.OnDatatransferProgressListener import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation import com.owncloud.android.lib.resources.files.model.RemoteFile +import com.owncloud.android.operations.RemoveFileOperation +import com.owncloud.android.operations.UploadFileOperation +import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.FileUtil +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import java.io.File -import java.util.Optional +import java.util.concurrent.Semaphore import javax.inject.Inject @Suppress("TooManyFunctions") @@ -45,6 +60,11 @@ class FileUploadHelper { @Inject lateinit var uploadsStorageManager: UploadsStorageManager + @Inject + lateinit var fileStorageManager: FileDataStorageManager + + private val ioScope = CoroutineScope(Dispatchers.IO) + init { MainApp.getAppComponent().inject(this) } @@ -59,36 +79,66 @@ class FileUploadHelper { private var instance: FileUploadHelper? = null - fun instance(): FileUploadHelper { - return instance ?: synchronized(this) { - instance ?: FileUploadHelper().also { instance = it } - } + private val retryFailedUploadsSemaphore = Semaphore(1) + + fun instance(): FileUploadHelper = instance ?: synchronized(this) { + instance ?: FileUploadHelper().also { instance = it } } - fun buildRemoteName(accountName: String, remotePath: String): String { - return accountName + remotePath - } + fun buildRemoteName(accountName: String, remotePath: String): String = accountName + remotePath } + /** + * Retries all failed uploads across all user accounts. + * + * This function retrieves all uploads with the status [UploadStatus.UPLOAD_FAILED], including both + * manual uploads and auto uploads. It runs in a background thread (Dispatcher.IO) and ensures + * that only one retry operation runs at a time by using a semaphore to prevent concurrent execution. + * + * Once the failed uploads are retrieved, it calls [retryUploads], which triggers the corresponding + * upload workers for each failed upload. + * + * The function returns `true` if there were any failed uploads to retry and the retry process was + * started, or `false` if no uploads were retried. + * + * @param uploadsStorageManager Provides access to upload data and persistence. + * @param connectivityService Checks the current network connectivity state. + * @param accountManager Handles user account authentication and selection. + * @param powerManagementService Ensures uploads respect power constraints. + * @return `true` if any failed uploads were found and retried; `false` otherwise. + */ fun retryFailedUploads( uploadsStorageManager: UploadsStorageManager, connectivityService: ConnectivityService, accountManager: UserAccountManager, powerManagementService: PowerManagementService - ) { - val failedUploads = uploadsStorageManager.failedUploads - if (failedUploads == null || failedUploads.isEmpty()) { - Log_OC.d(TAG, "Failed uploads are empty or null") - return + ): Boolean { + if (!retryFailedUploadsSemaphore.tryAcquire()) { + Log_OC.d(TAG, "skipping retryFailedUploads, already running") + return true } - retryUploads( - uploadsStorageManager, - connectivityService, - accountManager, - powerManagementService, - failedUploads - ) + var isUploadStarted = false + + try { + getUploadsByStatus(null, UploadStatus.UPLOAD_FAILED) { + if (it.isNotEmpty()) { + isUploadStarted = true + } + + retryUploads( + uploadsStorageManager, + connectivityService, + accountManager, + powerManagementService, + uploads = it + ) + } + } finally { + retryFailedUploadsSemaphore.release() + } + + return isUploadStarted } fun retryCancelledUploads( @@ -97,18 +147,18 @@ class FileUploadHelper { accountManager: UserAccountManager, powerManagementService: PowerManagementService ): Boolean { - val cancelledUploads = uploadsStorageManager.cancelledUploadsForCurrentAccount - if (cancelledUploads == null || cancelledUploads.isEmpty()) { - return false + var result = false + getUploadsByStatus(accountManager.user.accountName, UploadStatus.UPLOAD_CANCELLED) { + result = retryUploads( + uploadsStorageManager, + connectivityService, + accountManager, + powerManagementService, + it + ) } - return retryUploads( - uploadsStorageManager, - connectivityService, - accountManager, - powerManagementService, - cancelledUploads - ) + return result } @Suppress("ComplexCondition") @@ -117,40 +167,57 @@ class FileUploadHelper { connectivityService: ConnectivityService, accountManager: UserAccountManager, powerManagementService: PowerManagementService, - failedUploads: Array + uploads: Array ): Boolean { var showNotExistMessage = false - val (gotNetwork, _, gotWifi) = connectivityService.connectivity + val isOnline = checkConnectivity(connectivityService) + val connectivity = connectivityService.connectivity val batteryStatus = powerManagementService.battery - val charging = batteryStatus.isCharging || batteryStatus.isFull - val isPowerSaving = powerManagementService.isPowerSavingEnabled - var uploadUser = Optional.empty() - for (failedUpload in failedUploads) { - // 1. extract failed upload owner account and cache it between loops (expensive query) - if (!uploadUser.isPresent || !uploadUser.get().nameEquals(failedUpload.accountName)) { - uploadUser = accountManager.getUser(failedUpload.accountName) - } - val isDeleted = !File(failedUpload.localPath).exists() - if (isDeleted) { - showNotExistMessage = true + val uploadsToRetry = mutableListOf() - // 2A. for deleted files, mark as permanently failed - if (failedUpload.lastResult != UploadResult.FILE_NOT_FOUND) { - failedUpload.lastResult = UploadResult.FILE_NOT_FOUND - uploadsStorageManager.updateUpload(failedUpload) + for (upload in uploads) { + val uploadResult = checkUploadConditions( + upload, + connectivity, + batteryStatus, + powerManagementService, + isOnline + ) + + if (uploadResult != UploadResult.UPLOADED) { + if (upload.lastResult != uploadResult) { + // Setting Upload status else cancelled uploads will behave wrong, when retrying + // Needs to happen first since lastResult wil be overwritten by setter + upload.uploadStatus = UploadStatus.UPLOAD_FAILED + + upload.lastResult = uploadResult + uploadsStorageManager.updateUpload(upload) } - } else if (!isPowerSaving && gotNetwork && - canUploadBeRetried(failedUpload, gotWifi, charging) && !connectivityService.isInternetWalled - ) { - // 2B. for existing local files, try restarting it if possible - retryUpload(failedUpload, uploadUser.get()) + if (uploadResult == UploadResult.FILE_NOT_FOUND) { + showNotExistMessage = true + } + continue } + + // Only uploads that passed checks get marked in progress and are collected for scheduling + upload.uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS + uploadsStorageManager.updateUpload(upload) + uploadsToRetry.add(upload.uploadId) + } + + if (uploadsToRetry.isNotEmpty()) { + backgroundJobManager.startFilesUploadJob( + accountManager.user, + uploadsToRetry.toLongArray(), + false + ) } return showNotExistMessage } + @JvmOverloads @Suppress("LongParameterList") fun uploadNewFiles( user: User, @@ -161,10 +228,11 @@ class FileUploadHelper { createdBy: Int, requiresWifi: Boolean, requiresCharging: Boolean, - nameCollisionPolicy: NameCollisionPolicy + nameCollisionPolicy: NameCollisionPolicy, + showSameFileAlreadyExistsNotification: Boolean = true ) { val uploads = localPaths.mapIndexed { index, localPath -> - OCUpload(localPath, remotePaths[index], user.accountName).apply { + val result = OCUpload(localPath, remotePaths[index], user.accountName).apply { this.nameCollisionPolicy = nameCollisionPolicy isUseWifiOnly = requiresWifi isWhileChargingOnly = requiresCharging @@ -173,55 +241,121 @@ class FileUploadHelper { isCreateRemoteFolder = createRemoteFolder localAction = localBehavior } + + val id = uploadsStorageManager.uploadDao.insertOrReplace(result.toUploadEntity()) + result.uploadId = id + result } - uploadsStorageManager.storeUploads(uploads) - backgroundJobManager.startFilesUploadJob(user) + backgroundJobManager.startFilesUploadJob(user, uploads.getUploadIds(), showSameFileAlreadyExistsNotification) } fun removeFileUpload(remotePath: String, accountName: String) { - try { - val user = accountManager.getUser(accountName).get() + uploadsStorageManager.uploadDao.deleteByAccountAndRemotePath(accountName, remotePath) + } - // need to update now table in mUploadsStorageManager, - // since the operation will not get to be run by FileUploader#uploadFile - uploadsStorageManager.removeUpload(accountName, remotePath) - - cancelAndRestartUploadJob(user) - } catch (e: NoSuchElementException) { - Log_OC.e(TAG, "Error cancelling current upload because user does not exist!") + fun updateUploadStatus(remotePath: String, accountName: String, status: UploadStatus) { + ioScope.launch { + uploadsStorageManager.uploadDao.updateStatus(remotePath, accountName, status.value) } } - fun cancelFileUpload(remotePath: String, accountName: String) { - uploadsStorageManager.getUploadByRemotePath(remotePath).run { - removeFileUpload(remotePath, accountName) - uploadStatus = UploadStatus.UPLOAD_CANCELLED - uploadsStorageManager.storeUpload(this) + /** + * Retrieves uploads filtered by their status, optionally for a specific account. + * + * This function queries the uploads database asynchronously to obtain a list of uploads + * that match the specified [status]. If an [accountName] is provided, only uploads + * belonging to that account are retrieved. If [accountName] is `null`, uploads with the + * given [status] from **all user accounts** are returned. + * + * Once the uploads are fetched, the [onCompleted] callback is invoked with the resulting array. + * + * @param accountName The name of the account to filter uploads by. + * If `null`, uploads matching the given [status] from all accounts are returned. + * @param status The [UploadStatus] to filter uploads by (e.g., `UPLOAD_FAILED`). + * @param nameCollisionPolicy The [NameCollisionPolicy] to filter uploads by (e.g., `SKIP`). + * @param onCompleted A callback invoked with the resulting array of [OCUpload] objects. + */ + fun getUploadsByStatus( + accountName: String?, + status: UploadStatus, + nameCollisionPolicy: NameCollisionPolicy? = null, + onCompleted: (Array) -> Unit + ) { + ioScope.launch { + val dao = uploadsStorageManager.uploadDao + val result = if (accountName != null) { + dao.getUploadsByAccountNameAndStatus(accountName, status.value, nameCollisionPolicy?.serialize()) + } else { + dao.getUploadsByStatus(status.value, nameCollisionPolicy?.serialize()) + }.map { it.toOCUpload(null) }.toTypedArray() + onCompleted(result) } } - fun cancelAndRestartUploadJob(user: User) { + fun cancelAndRestartUploadJob(user: User, uploadIds: LongArray) { backgroundJobManager.run { cancelFilesUploadJob(user) - startFilesUploadJob(user) + startFilesUploadJob(user, uploadIds, false) } } @Suppress("ReturnCount") - fun isUploading(user: User?, file: OCFile?): Boolean { - if (user == null || file == null || !backgroundJobManager.isStartFileUploadJobScheduled(user)) { + fun isUploading(remotePath: String?, accountName: String?): Boolean { + accountName ?: return false + if (!backgroundJobManager.isStartFileUploadJobScheduled(accountName)) { return false } - val upload: OCUpload = uploadsStorageManager.getUploadByRemotePath(file.remotePath) ?: return false - return upload.uploadStatus == UploadStatus.UPLOAD_IN_PROGRESS + remotePath ?: return false + val upload = uploadsStorageManager.uploadDao.getByRemotePath(remotePath) + return upload?.status == UploadStatus.UPLOAD_IN_PROGRESS.value || + FileUploadWorker.isUploading(remotePath, accountName) } - private fun canUploadBeRetried(upload: OCUpload, gotWifi: Boolean, isCharging: Boolean): Boolean { - val file = File(upload.localPath) - val needsWifi = upload.isUseWifiOnly - val needsCharging = upload.isWhileChargingOnly - return file.exists() && (!needsWifi || gotWifi) && (!needsCharging || isCharging) + private fun checkConnectivity(connectivityService: ConnectivityService): Boolean { + // check that connection isn't walled off and that the server is reachable + return connectivityService.getConnectivity().isConnected && !connectivityService.isInternetWalled() + } + + /** + * Dupe of [UploadFileOperation.checkConditions], needed to check if the upload should even be scheduled + * @return [UploadResult.UPLOADED] if the upload should be scheduled, otherwise the reason why it shouldn't + */ + private fun checkUploadConditions( + upload: OCUpload, + connectivity: Connectivity, + battery: BatteryStatus, + powerManagementService: PowerManagementService, + hasGeneralConnection: Boolean + ): UploadResult { + var conditions = UploadResult.UPLOADED + + // check that internet is available + if (!hasGeneralConnection) { + conditions = UploadResult.NETWORK_CONNECTION + } + + // check that local file exists; skip the upload otherwise + if (!File(upload.localPath).exists()) { + conditions = UploadResult.FILE_NOT_FOUND + } + + // check that connectivity conditions are met; delay upload otherwise + if (upload.isUseWifiOnly && (!connectivity.isWifi || connectivity.isMetered)) { + conditions = UploadResult.DELAYED_FOR_WIFI + } + + // check if charging conditions are met; delay upload otherwise + if (upload.isWhileChargingOnly && !battery.isCharging && !battery.isFull) { + conditions = UploadResult.DELAYED_FOR_CHARGING + } + + // check that device is not in power save mode; delay upload otherwise + if (powerManagementService.isPowerSavingEnabled) { + conditions = UploadResult.DELAYED_IN_POWER_SAVE_MODE + } + + return conditions } @Suppress("ReturnCount") @@ -254,7 +388,7 @@ class FileUploadHelper { val uploads = existingFiles.map { file -> file?.let { - OCUpload(file, user).apply { + val result = OCUpload(file, user).apply { fileSize = file.fileLength this.nameCollisionPolicy = nameCollisionPolicy isCreateRemoteFolder = true @@ -263,10 +397,49 @@ class FileUploadHelper { isWhileChargingOnly = false uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS } + + val id = uploadsStorageManager.uploadDao.insertOrReplace(result.toUploadEntity()) + result.uploadId = id + result + } + } + val uploadIds: LongArray = uploads.filterNotNull().map { it.uploadId }.toLongArray() + backgroundJobManager.startFilesUploadJob(user, uploadIds, true) + } + + /** + * Removes any existing file in the same directory that has the same name as the provided new file. + * + * This function checks the parent directory of the given `newFile` for any file with the same name. + * If such a file is found, it is removed using the `RemoveFileOperation`. + * + * @param duplicatedFile File to be deleted + * @param client Needed for executing RemoveFileOperation + * @param user Needed for creating client + */ + fun removeDuplicatedFile(duplicatedFile: OCFile, client: OwnCloudClient, user: User, onCompleted: () -> Unit) { + val job = CoroutineScope(Dispatchers.IO) + + job.launch { + val removeFileOperation = RemoveFileOperation( + duplicatedFile, + false, + user, + true, + MainApp.getAppContext(), + fileStorageManager + ) + + val result = removeFileOperation.execute(client) + + if (result.isSuccess) { + Log_OC.d(TAG, "Replaced file successfully removed") + + launch(Dispatchers.Main) { + onCompleted() + } } } - uploadsStorageManager.storeUploads(uploads) - backgroundJobManager.startFilesUploadJob(user) } fun retryUpload(upload: OCUpload, user: User) { @@ -275,25 +448,20 @@ class FileUploadHelper { upload.uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS uploadsStorageManager.updateUpload(upload) - backgroundJobManager.startFilesUploadJob(user) + backgroundJobManager.startFilesUploadJob(user, longArrayOf(upload.uploadId), false) } fun cancel(accountName: String) { uploadsStorageManager.removeUploads(accountName) - cancelAndRestartUploadJob(accountManager.getUser(accountName).get()) + val uploadIds = uploadsStorageManager.getCurrentUploadIds(accountName) + cancelAndRestartUploadJob(accountManager.getUser(accountName).get(), uploadIds) } - fun addUploadTransferProgressListener( - listener: OnDatatransferProgressListener, - targetKey: String - ) { + fun addUploadTransferProgressListener(listener: OnDatatransferProgressListener, targetKey: String) { mBoundListeners[targetKey] = listener } - fun removeUploadTransferProgressListener( - listener: OnDatatransferProgressListener, - targetKey: String - ) { + fun removeUploadTransferProgressListener(listener: OnDatatransferProgressListener, targetKey: String) { if (mBoundListeners[targetKey] === listener) { mBoundListeners.remove(targetKey) } @@ -318,6 +486,14 @@ class FileUploadHelper { return false } + fun showFileUploadLimitMessage(activity: Activity) { + val message = activity.resources.getQuantityString( + R.plurals.file_upload_limit_message, + MAX_FILE_COUNT + ) + DisplayUtils.showSnackMessage(activity, message) + } + class UploadNotificationActionReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val accountName = intent.getStringExtra(FileUploadWorker.EXTRA_ACCOUNT_NAME) @@ -333,7 +509,9 @@ class FileUploadHelper { return } - instance().cancelFileUpload(remotePath, accountName) + FileUploadWorker.cancelCurrentUpload(remotePath, accountName, onCompleted = { + instance().updateUploadStatus(remotePath, accountName, UploadStatus.UPLOAD_CANCELLED) + }) } } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt index 80cb01e..093a3a9 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt @@ -1,16 +1,18 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs.upload +import android.app.Notification import android.app.PendingIntent import android.content.Context +import androidx.core.app.NotificationCompat import androidx.localbroadcastmanager.content.LocalBroadcastManager -import androidx.work.Worker +import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager @@ -21,20 +23,31 @@ import com.nextcloud.client.network.ConnectivityService import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.model.WorkerState import com.nextcloud.model.WorkerStateLiveData +import com.nextcloud.utils.ForegroundServiceHelper +import com.nextcloud.utils.extensions.getPercent +import com.nextcloud.utils.extensions.updateStatus +import com.owncloud.android.R import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.ForegroundServiceType import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.db.OCUpload import com.owncloud.android.lib.common.OwnCloudAccount +import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.network.OnDatatransferProgressListener import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.UploadFileOperation +import com.owncloud.android.ui.notifications.NotificationUtils import com.owncloud.android.utils.ErrorMessageAdapter import com.owncloud.android.utils.theme.ViewThemeUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.withContext import java.io.File +import kotlin.random.Random @Suppress("LongParameterList") class FileUploadWorker( @@ -48,20 +61,28 @@ class FileUploadWorker( val preferences: AppPreferences, val context: Context, params: WorkerParameters -) : Worker(context, params), OnDatatransferProgressListener { +) : CoroutineWorker(context, params), + OnDatatransferProgressListener { companion object { val TAG: String = FileUploadWorker::class.java.simpleName const val NOTIFICATION_ERROR_ID: Int = 413 - private const val MAX_PROGRESS: Int = 100 + const val ACCOUNT = "data_account" + const val UPLOAD_IDS = "uploads_ids" + const val CURRENT_BATCH_INDEX = "batch_index" + const val TOTAL_UPLOAD_SIZE = "total_upload_size" + const val SHOW_SAME_FILE_ALREADY_EXISTS_NOTIFICATION = "show_same_file_already_exists_notification" + var currentUploadFileOperation: UploadFileOperation? = null private const val UPLOADS_ADDED_MESSAGE = "UPLOADS_ADDED" private const val UPLOAD_START_MESSAGE = "UPLOAD_START" private const val UPLOAD_FINISH_MESSAGE = "UPLOAD_FINISH" + private const val BATCH_SIZE = 100 + const val EXTRA_UPLOAD_RESULT = "RESULT" const val EXTRA_REMOTE_PATH = "REMOTE_PATH" const val EXTRA_OLD_REMOTE_PATH = "OLD_REMOTE_PATH" @@ -75,151 +96,242 @@ class FileUploadWorker( const val LOCAL_BEHAVIOUR_FORGET = 2 const val LOCAL_BEHAVIOUR_DELETE = 3 - fun getUploadsAddedMessage(): String { - return FileUploadWorker::class.java.name + UPLOADS_ADDED_MESSAGE + fun getUploadsAddedMessage(): String = FileUploadWorker::class.java.name + UPLOADS_ADDED_MESSAGE + + fun getUploadStartMessage(): String = FileUploadWorker::class.java.name + UPLOAD_START_MESSAGE + + fun getUploadFinishMessage(): String = FileUploadWorker::class.java.name + UPLOAD_FINISH_MESSAGE + + fun cancelCurrentUpload(remotePath: String, accountName: String, onCompleted: () -> Unit) { + currentUploadFileOperation?.let { + if (it.remotePath == remotePath && it.user.accountName == accountName) { + it.cancel(ResultCode.USER_CANCELLED) + onCompleted() + } + } } - fun getUploadStartMessage(): String { - return FileUploadWorker::class.java.name + UPLOAD_START_MESSAGE - } + fun isUploading(remotePath: String?, accountName: String?): Boolean { + currentUploadFileOperation?.let { + return it.remotePath == remotePath && it.user.accountName == accountName + } - fun getUploadFinishMessage(): String { - return FileUploadWorker::class.java.name + UPLOAD_FINISH_MESSAGE + return false } } private var lastPercent = 0 - private val notificationManager = UploadNotificationManager(context, viewThemeUtils) + private val notificationId = Random.nextInt() + private val notificationManager = UploadNotificationManager(context, viewThemeUtils, notificationId) private val intents = FileUploaderIntents(context) private val fileUploaderDelegate = FileUploaderDelegate() @Suppress("TooGenericExceptionCaught") - override fun doWork(): Result { - return try { - backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class)) - val result = retrievePagesBySortingUploadsByID() - backgroundJobManager.logEndOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class), result) - result - } catch (t: Throwable) { - Log_OC.e(TAG, "Error caught at FileUploadWorker " + t.localizedMessage) - Result.failure() + override suspend fun doWork(): Result = try { + Log_OC.d(TAG, "FileUploadWorker started") + val workerName = BackgroundJobManagerImpl.formatClassTag(this::class) + backgroundJobManager.logStartOfWorker(workerName) + + val notificationTitle = notificationManager.currentOperationTitle + ?: context.getString(R.string.foreground_service_upload) + val notification = createNotification(notificationTitle) + updateForegroundInfo(notification) + + val result = uploadFiles() + backgroundJobManager.logEndOfWorker(workerName, result) + notificationManager.dismissNotification() + if (result == Result.success()) { + setIdleWorkerState() } + result + } catch (t: Throwable) { + Log_OC.e(TAG, "Error caught at FileUploadWorker $t") + cleanup() + Result.failure() } - override fun onStopped() { + private suspend fun updateForegroundInfo(notification: Notification) { + val foregroundInfo = ForegroundServiceHelper.createWorkerForegroundInfo( + notificationId, + notification, + ForegroundServiceType.DataSync + ) + setForeground(foregroundInfo) + } + + private fun createNotification(title: String): Notification = + NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD) + .setContentTitle(title) + .setSmallIcon(R.drawable.uploads) + .setOngoing(true) + .setSound(null) + .setVibrate(null) + .setOnlyAlertOnce(true) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setSilent(true) + .build() + + private fun cleanup() { Log_OC.e(TAG, "FileUploadWorker stopped") setIdleWorkerState() currentUploadFileOperation?.cancel(null) - notificationManager.dismissWorkerNotifications() - - super.onStopped() + notificationManager.dismissNotification() } - private fun setWorkerState(user: User?, uploads: List) { - WorkerStateLiveData.instance().setWorkState(WorkerState.Upload(user, uploads)) + private fun setWorkerState(user: User?) { + WorkerStateLiveData.instance().setWorkState(WorkerState.UploadStarted(user)) } private fun setIdleWorkerState() { - WorkerStateLiveData.instance().setWorkState(WorkerState.Idle) + WorkerStateLiveData.instance().setWorkState(WorkerState.UploadFinished(currentUploadFileOperation?.file)) } - @Suppress("ReturnCount") - private fun retrievePagesBySortingUploadsByID(): Result { - val accountName = inputData.getString(ACCOUNT) ?: return Result.failure() - var currentPage = uploadsStorageManager.getCurrentAndPendingUploadsForAccountPageAscById(-1, accountName) + @Suppress("ReturnCount", "LongMethod") + private suspend fun uploadFiles(): Result = withContext(Dispatchers.IO) { + val accountName = inputData.getString(ACCOUNT) + if (accountName == null) { + Log_OC.e(TAG, "accountName is null") + return@withContext Result.failure() + } - notificationManager.dismissWorkerNotifications() + val uploadIds = inputData.getLongArray(UPLOAD_IDS) + if (uploadIds == null) { + Log_OC.e(TAG, "uploadIds is null") + return@withContext Result.failure() + } + + val currentBatchIndex = inputData.getInt(CURRENT_BATCH_INDEX, -1) + if (currentBatchIndex == -1) { + Log_OC.e(TAG, "currentBatchIndex is -1, cancelling") + return@withContext Result.failure() + } + + val totalUploadSize = inputData.getInt(TOTAL_UPLOAD_SIZE, -1) + if (totalUploadSize == -1) { + Log_OC.e(TAG, "totalUploadSize is -1, cancelling") + return@withContext Result.failure() + } + + // since worker's policy is append or replace and account name comes from there no need check in the loop + val optionalUser = userAccountManager.getUser(accountName) + if (!optionalUser.isPresent) { + Log_OC.e(TAG, "User not found for account: $accountName") + return@withContext Result.failure() + } + + val user = optionalUser.get() + val previouslyUploadedFileSize = currentBatchIndex * FileUploadHelper.MAX_FILE_COUNT + val uploads = uploadsStorageManager.getUploadsByIds(uploadIds, accountName) + val ocAccount = OwnCloudAccount(user.toPlatformAccount(), context) + val client = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, context) + + for ((index, upload) in uploads.withIndex()) { + ensureActive() - while (currentPage.isNotEmpty() && !isStopped) { if (preferences.isGlobalUploadPaused) { Log_OC.d(TAG, "Upload is paused, skip uploading files!") notificationManager.notifyPaused( intents.notificationStartIntent(null) ) - return Result.success() + return@withContext Result.success() } - Log_OC.d(TAG, "Handling ${currentPage.size} uploads for account $accountName") - val lastId = currentPage.last().uploadId - uploadFiles(currentPage, accountName) - currentPage = - uploadsStorageManager.getCurrentAndPendingUploadsForAccountPageAscById(lastId, accountName) + if (canExitEarly()) { + notificationManager.showConnectionErrorNotification() + return@withContext Result.failure() + } + + setWorkerState(user) + val operation = createUploadFileOperation(upload, user) + currentUploadFileOperation = operation + + val currentIndex = (index + 1) + val currentUploadIndex = (currentIndex + previouslyUploadedFileSize) + notificationManager.prepareForStart( + operation, + cancelPendingIntent = intents.startIntent(operation), + startIntent = intents.notificationStartIntent(operation), + currentUploadIndex = currentUploadIndex, + totalUploadSize = totalUploadSize + ) + + val result = withContext(Dispatchers.IO) { + upload(operation, user, client) + } + val entity = uploadsStorageManager.uploadDao.getUploadById(upload.uploadId, accountName) + uploadsStorageManager.updateStatus(entity, result.isSuccess) + currentUploadFileOperation = null + sendUploadFinishEvent(totalUploadSize, currentUploadIndex, operation, result) } - if (isStopped) { - Log_OC.d(TAG, "FileUploadWorker for account $accountName was stopped") + return@withContext Result.success() + } + + private fun sendUploadFinishEvent( + totalUploadSize: Int, + currentUploadIndex: Int, + operation: UploadFileOperation, + result: RemoteOperationResult<*> + ) { + val shouldBroadcast = + (totalUploadSize > BATCH_SIZE && currentUploadIndex > 0) && currentUploadIndex % BATCH_SIZE == 0 + + if (shouldBroadcast) { + // delay broadcast + fileUploaderDelegate.sendBroadcastUploadFinished( + operation, + result, + operation.oldFile?.storagePath, + context, + localBroadcastManager + ) + } + } + + private fun canExitEarly(): Boolean { + val result = !connectivityService.isConnected || + connectivityService.isInternetWalled || + isStopped + + if (result) { + Log_OC.d(TAG, "No internet connection, stopping worker.") } else { - Log_OC.d(TAG, "No more pending uploads for account $accountName, stopping work") + notificationManager.dismissErrorNotification() } - return Result.success() + + return result } - private fun uploadFiles(uploads: List, accountName: String) { - val user = userAccountManager.getUser(accountName) - setWorkerState(user.get(), uploads) - - for (upload in uploads) { - if (isStopped) { - break - } - - if (user.isPresent) { - val uploadFileOperation = createUploadFileOperation(upload, user.get()) - - currentUploadFileOperation = uploadFileOperation - val result = upload(uploadFileOperation, user.get()) - currentUploadFileOperation = null - - fileUploaderDelegate.sendBroadcastUploadFinished( - uploadFileOperation, - result, - uploadFileOperation.oldFile?.storagePath, - context, - localBroadcastManager - ) - } else { - uploadsStorageManager.removeUpload(upload.uploadId) - } - } - } - - private fun createUploadFileOperation(upload: OCUpload, user: User): UploadFileOperation { - return UploadFileOperation( - uploadsStorageManager, - connectivityService, - powerManagementService, - user, - null, - upload, - upload.nameCollisionPolicy, - upload.localAction, - context, - upload.isUseWifiOnly, - upload.isWhileChargingOnly, - true, - FileDataStorageManager(user, context.contentResolver) - ).apply { - addDataTransferProgressListener(this@FileUploadWorker) - } + private fun createUploadFileOperation(upload: OCUpload, user: User): UploadFileOperation = UploadFileOperation( + uploadsStorageManager, + connectivityService, + powerManagementService, + user, + null, + upload, + upload.nameCollisionPolicy, + upload.localAction, + context, + upload.isUseWifiOnly, + upload.isWhileChargingOnly, + true, + FileDataStorageManager(user, context.contentResolver) + ).apply { + addDataTransferProgressListener(this@FileUploadWorker) } @Suppress("TooGenericExceptionCaught", "DEPRECATION") - private fun upload(uploadFileOperation: UploadFileOperation, user: User): RemoteOperationResult { + private fun upload( + uploadFileOperation: UploadFileOperation, + user: User, + client: OwnCloudClient + ): RemoteOperationResult { lateinit var result: RemoteOperationResult - notificationManager.prepareForStart( - uploadFileOperation, - cancelPendingIntent = intents.startIntent(uploadFileOperation), - intents.notificationStartIntent(uploadFileOperation) - ) - try { val storageManager = uploadFileOperation.storageManager - val ocAccount = OwnCloudAccount(user.toPlatformAccount(), context) - val uploadClient = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, context) - result = uploadFileOperation.execute(uploadClient) - + result = uploadFileOperation.execute(client) val task = ThumbnailsCacheManager.ThumbnailGenerationTask(storageManager, user) val file = File(uploadFileOperation.originalStoragePath) val remoteId: String? = uploadFileOperation.file.remoteId @@ -238,16 +350,17 @@ class FileUploadWorker( if (!isStopped || !result.isCancelled) { uploadsStorageManager.updateDatabaseUploadResult(result, uploadFileOperation) notifyUploadResult(uploadFileOperation, result) - notificationManager.dismissWorkerNotifications() } } - @Suppress("ReturnCount") + @Suppress("ReturnCount", "LongMethod") private fun notifyUploadResult( uploadFileOperation: UploadFileOperation, uploadResult: RemoteOperationResult ) { Log_OC.d(TAG, "NotifyUploadResult with resultCode: " + uploadResult.code) + val showSameFileAlreadyExistsNotification = + inputData.getBoolean(SHOW_SAME_FILE_ALREADY_EXISTS_NOTIFICATION, false) if (uploadResult.isSuccess) { notificationManager.dismissOldErrorNotification(uploadFileOperation) @@ -259,10 +372,19 @@ class FileUploadWorker( } // Only notify if it is not same file on remote that causes conflict - if (uploadResult.code == ResultCode.SYNC_CONFLICT && FileUploadHelper().isSameFileOnRemote( - uploadFileOperation.user, File(uploadFileOperation.storagePath), uploadFileOperation.remotePath, context + if (uploadResult.code == ResultCode.SYNC_CONFLICT && + FileUploadHelper().isSameFileOnRemote( + uploadFileOperation.user, + File(uploadFileOperation.storagePath), + uploadFileOperation.remotePath, + context ) ) { + if (showSameFileAlreadyExistsNotification) { + notificationManager.showSameFileAlreadyExistsNotification(uploadFileOperation.fileName) + } + + uploadFileOperation.handleLocalBehaviour() return } @@ -281,6 +403,10 @@ class FileUploadWorker( return } + if (uploadResult.code == ResultCode.USER_CANCELLED) { + return + } + notificationManager.run { val errorMessage = ErrorMessageAdapter.getErrorCauseMessage( uploadResult, @@ -300,31 +426,51 @@ class FileUploadWorker( null } - notifyForFailedResult(uploadResult.code, conflictResolveIntent, credentialIntent, errorMessage) - showNewNotification(uploadFileOperation) + val cancelUploadActionIntent = if (conflictResolveIntent != null) { + intents.cancelUploadActionIntent(uploadFileOperation) + } else { + null + } + + notifyForFailedResult( + uploadFileOperation, + uploadResult.code, + conflictResolveIntent, + cancelUploadActionIntent, + credentialIntent, + errorMessage + ) } } + @Suppress("MagicNumber") + private val minProgressUpdateInterval = 750 + private var lastUpdateTime = 0L + + /** + * Receives from [com.owncloud.android.operations.UploadFileOperation.normalUpload] + */ + @Suppress("MagicNumber") override fun onTransferProgress( progressRate: Long, totalTransferredSoFar: Long, totalToTransfer: Long, fileAbsoluteName: String ) { - val percent = (MAX_PROGRESS * totalTransferredSoFar.toDouble() / totalToTransfer.toDouble()).toInt() + val percent = getPercent(totalTransferredSoFar, totalToTransfer) + val currentTime = System.currentTimeMillis() - if (percent != lastPercent) { + if (percent != lastPercent && (currentTime - lastUpdateTime) >= minProgressUpdateInterval) { notificationManager.run { val accountName = currentUploadFileOperation?.user?.accountName val remotePath = currentUploadFileOperation?.remotePath - val filename = currentUploadFileOperation?.fileName ?: "" - updateUploadProgress(filename, percent, currentUploadFileOperation) + updateUploadProgress(percent, currentUploadFileOperation) if (accountName != null && remotePath != null) { - val key: String = - FileUploadHelper.buildRemoteName(accountName, remotePath) + val key: String = FileUploadHelper.buildRemoteName(accountName, remotePath) val boundListener = FileUploadHelper.mBoundListeners[key] + val filename = currentUploadFileOperation?.fileName ?: "" boundListener?.onTransferProgress( progressRate, @@ -336,6 +482,7 @@ class FileUploadWorker( dismissOldErrorNotification(currentUploadFileOperation) } + lastUpdateTime = currentTime } lastPercent = percent diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploaderDelegate.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploaderDelegate.kt index d9d3726..2cff0b4 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploaderDelegate.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploaderDelegate.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs.upload diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploaderIntents.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploaderIntents.kt index 13fd8b8..b99310f 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploaderIntents.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploaderIntents.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs.upload @@ -12,7 +12,6 @@ import android.content.Context import android.content.Intent import android.os.Build import com.owncloud.android.authentication.AuthenticatorActivity -import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.ui.activity.ConflictsResolveActivity.Companion.createIntent import com.owncloud.android.ui.activity.UploadListActivity @@ -57,32 +56,6 @@ class FileUploaderIntents(private val context: Context) { ) } - fun resultIntent(resultCode: ResultCode, operation: UploadFileOperation): PendingIntent { - val intent = if (resultCode == ResultCode.SYNC_CONFLICT) { - createIntent( - operation.file, - operation.user, - operation.ocUploadId, - Intent.FLAG_ACTIVITY_CLEAR_TOP, - context - ) - } else { - UploadListActivity.createIntent( - operation.file, - operation.user, - Intent.FLAG_ACTIVITY_CLEAR_TOP, - context - ) - } - - return PendingIntent.getActivity( - context, - System.currentTimeMillis().toInt(), - intent, - PendingIntent.FLAG_IMMUTABLE - ) - } - fun notificationStartIntent(operation: UploadFileOperation?): PendingIntent { val intent = UploadListActivity.createIntent( operation?.file, @@ -119,4 +92,19 @@ class FileUploaderIntents(private val context: Context) { ) } } + + fun cancelUploadActionIntent(uploadFileOperation: UploadFileOperation): PendingIntent { + val intent = Intent(context, FileUploadBroadcastReceiver::class.java).apply { + putExtra(FileUploadBroadcastReceiver.UPLOAD_ID, uploadFileOperation.ocUploadId) + putExtra(FileUploadBroadcastReceiver.REMOTE_PATH, uploadFileOperation.file.remotePath) + putExtra(FileUploadBroadcastReceiver.STORAGE_PATH, uploadFileOperation.file.storagePath) + } + + return PendingIntent.getBroadcast( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/PostUploadAction.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/PostUploadAction.kt index 28e8eec..2bbf621 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/PostUploadAction.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/PostUploadAction.kt @@ -4,7 +4,7 @@ * @author Chris Narkiewicz * Copyright (C) 2021 Chris Narkiewicz * - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs.upload diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/UploadNotificationManager.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/UploadNotificationManager.kt index 4c2a9c6..4ba76bf 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/UploadNotificationManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/UploadNotificationManager.kt @@ -1,65 +1,58 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs.upload -import android.app.Notification -import android.app.NotificationManager import android.app.PendingIntent import android.content.Context -import android.graphics.BitmapFactory -import android.os.Build -import androidx.core.app.NotificationCompat +import com.nextcloud.client.jobs.notification.WorkerNotificationManager +import com.nextcloud.utils.extensions.isFileSpecificError +import com.nextcloud.utils.numberFormatter.NumberFormatter import com.owncloud.android.R import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.ui.notifications.NotificationUtils import com.owncloud.android.utils.theme.ViewThemeUtils -class UploadNotificationManager(private val context: Context, viewThemeUtils: ViewThemeUtils) { - companion object { - private const val ID = 411 - } - - private var notification: Notification? = null - private var notificationBuilder: NotificationCompat.Builder = - NotificationUtils.newNotificationBuilder(context, viewThemeUtils).apply { - setContentTitle(context.getString(R.string.foreground_service_upload)) - setSmallIcon(R.drawable.notification_icon) - setLargeIcon(BitmapFactory.decodeResource(context.resources, R.drawable.notification_icon)) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD) - } - } - private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - init { - notification = notificationBuilder.build() - } +class UploadNotificationManager(private val context: Context, viewThemeUtils: ViewThemeUtils, id: Int) : + WorkerNotificationManager( + id, + context, + viewThemeUtils, + tickerId = R.string.foreground_service_upload, + channelId = NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD + ) { @Suppress("MagicNumber") fun prepareForStart( uploadFileOperation: UploadFileOperation, cancelPendingIntent: PendingIntent, - startIntent: PendingIntent + startIntent: PendingIntent, + currentUploadIndex: Int, + totalUploadSize: Int ) { - notificationBuilder.run { - setContentTitle(context.getString(R.string.uploader_upload_in_progress_ticker)) - setContentText( - String.format( - context.getString(R.string.uploader_upload_in_progress), - 0, - uploadFileOperation.fileName - ) + currentOperationTitle = if (totalUploadSize > 1) { + String.format( + context.getString(R.string.upload_notification_manager_start_text), + currentUploadIndex, + totalUploadSize, + uploadFileOperation.fileName ) - setTicker(context.getString(R.string.foreground_service_upload)) + } else { + uploadFileOperation.fileName + } + + val progressText = NumberFormatter.getPercentageText(0) + + notificationBuilder.run { setProgress(100, 0, false) - setOngoing(true) + setContentTitle(currentOperationTitle) + setContentText(progressText) + setOngoing(false) clearActions() addAction( @@ -76,13 +69,27 @@ class UploadNotificationManager(private val context: Context, viewThemeUtils: Vi } } + @Suppress("MagicNumber") + fun updateUploadProgress(percent: Int, currentOperation: UploadFileOperation?) { + val progressText = NumberFormatter.getPercentageText(percent) + setProgress(percent, progressText, false) + showNotification() + dismissOldErrorNotification(currentOperation) + } + fun notifyForFailedResult( + uploadFileOperation: UploadFileOperation, resultCode: RemoteOperationResult.ResultCode, conflictsResolveIntent: PendingIntent?, + cancelUploadActionIntent: PendingIntent?, credentialIntent: PendingIntent?, errorMessage: String ) { - val textId = resultTitle(resultCode) + if (uploadFileOperation.isMissingPermissionThrown) { + return + } + + val textId = getFailedResultTitleId(resultCode) notificationBuilder.run { setTicker(context.getString(textId)) @@ -100,15 +107,29 @@ class UploadNotificationManager(private val context: Context, viewThemeUtils: Vi ) } + cancelUploadActionIntent?.let { + addAction( + R.drawable.ic_delete, + R.string.upload_list_cancel_upload, + cancelUploadActionIntent + ) + } + credentialIntent?.let { setContentIntent(it) } setContentText(errorMessage) } + + if (resultCode.isFileSpecificError()) { + showNewNotification(uploadFileOperation) + } else { + showNotification() + } } - private fun resultTitle(resultCode: RemoteOperationResult.ResultCode): Int { + private fun getFailedResultTitleId(resultCode: RemoteOperationResult.ResultCode): Int { val needsToUpdateCredentials = (resultCode == RemoteOperationResult.ResultCode.UNAUTHORIZED) return if (needsToUpdateCredentials) { @@ -128,7 +149,7 @@ class UploadNotificationManager(private val context: Context, viewThemeUtils: Vi ) } - fun showNewNotification(operation: UploadFileOperation) { + private fun showNewNotification(operation: UploadFileOperation) { notificationManager.notify( NotificationUtils.createUploadNotificationTag(operation.file), FileUploadWorker.NOTIFICATION_ERROR_ID, @@ -136,20 +157,35 @@ class UploadNotificationManager(private val context: Context, viewThemeUtils: Vi ) } - private fun showNotification() { - notificationManager.notify(ID, notificationBuilder.build()) + fun showSameFileAlreadyExistsNotification(filename: String) { + notificationBuilder.run { + setAutoCancel(true) + clearActions() + setContentText("") + setProgress(0, 0, false) + setContentTitle(context.getString(R.string.file_upload_worker_same_file_already_exists, filename)) + } + + val notificationId = filename.hashCode() + + notificationManager.notify( + notificationId, + notificationBuilder.build() + ) } - @Suppress("MagicNumber") - fun updateUploadProgress(filename: String, percent: Int, currentOperation: UploadFileOperation?) { - notificationBuilder.run { - setProgress(100, percent, false) - val text = String.format(context.getString(R.string.uploader_upload_in_progress), percent, filename) - setContentText(text) + fun showConnectionErrorNotification() { + notificationManager.cancel(getId()) - showNotification() - dismissOldErrorNotification(currentOperation) + notificationBuilder.run { + setContentTitle(context.getString(R.string.file_upload_worker_error_notification_title)) + setContentText("") } + + notificationManager.notify( + FileUploadWorker.NOTIFICATION_ERROR_ID, + notificationBuilder.build() + ) } fun dismissOldErrorNotification(operation: UploadFileOperation?) { @@ -164,6 +200,8 @@ class UploadNotificationManager(private val context: Context, viewThemeUtils: Vi } } + fun dismissErrorNotification() = notificationManager.cancel(FileUploadWorker.NOTIFICATION_ERROR_ID) + fun dismissOldErrorNotification(remotePath: String, localPath: String) { notificationManager.cancel( NotificationUtils.createUploadNotificationTag(remotePath, localPath), @@ -171,15 +209,11 @@ class UploadNotificationManager(private val context: Context, viewThemeUtils: Vi ) } - fun dismissWorkerNotifications() { - notificationManager.cancel(ID) - } - fun notifyPaused(intent: PendingIntent) { - notificationBuilder.apply { + notificationBuilder.run { setContentTitle(context.getString(R.string.upload_global_pause_title)) setTicker(context.getString(R.string.upload_global_pause_title)) - setOngoing(true) + setOngoing(false) setAutoCancel(false) setProgress(0, 0, false) clearActions() diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/UploadTask.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/UploadTask.kt index 08f4585..21aa361 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/UploadTask.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/UploadTask.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs.upload @@ -44,16 +44,14 @@ class UploadTask( private val clientProvider: () -> OwnCloudClient, private val fileDataStorageManager: FileDataStorageManager ) { - fun create(): UploadTask { - return UploadTask( - applicationContext, - uploadsStorageManager, - connectivityService, - powerManagementService, - clientProvider, - fileDataStorageManager - ) - } + fun create(): UploadTask = UploadTask( + applicationContext, + uploadsStorageManager, + connectivityService, + powerManagementService, + clientProvider, + fileDataStorageManager + ) } fun upload(user: User, upload: OCUpload): Result { diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/UploadTrigger.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/UploadTrigger.kt index 3712cb4..a378f59 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/UploadTrigger.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/UploadTrigger.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.jobs.upload diff --git a/app/src/main/java/com/nextcloud/client/logger/FileLogHandler.kt b/app/src/main/java/com/nextcloud/client/logger/FileLogHandler.kt index ef01ca9..dc1409d 100644 --- a/app/src/main/java/com/nextcloud/client/logger/FileLogHandler.kt +++ b/app/src/main/java/com/nextcloud/client/logger/FileLogHandler.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.logger diff --git a/app/src/main/java/com/nextcloud/client/logger/LegacyLoggerAdapter.kt b/app/src/main/java/com/nextcloud/client/logger/LegacyLoggerAdapter.kt index 3c7805e..0d9f845 100644 --- a/app/src/main/java/com/nextcloud/client/logger/LegacyLoggerAdapter.kt +++ b/app/src/main/java/com/nextcloud/client/logger/LegacyLoggerAdapter.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.logger diff --git a/app/src/main/java/com/nextcloud/client/logger/Level.kt b/app/src/main/java/com/nextcloud/client/logger/Level.kt index 9aa2562..cac864e 100644 --- a/app/src/main/java/com/nextcloud/client/logger/Level.kt +++ b/app/src/main/java/com/nextcloud/client/logger/Level.kt @@ -2,10 +2,12 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.logger +import com.owncloud.android.R + enum class Level(val tag: String) { UNKNOWN("U"), VERBOSE("V"), @@ -15,6 +17,16 @@ enum class Level(val tag: String) { ERROR("E"), ASSERT("A"); + fun getColor(): Int = when (this) { + UNKNOWN -> R.color.log_level_unknown + VERBOSE -> R.color.log_level_verbose + DEBUG -> R.color.log_level_debug + INFO -> R.color.log_level_info + WARNING -> R.color.log_level_warning + ASSERT -> R.color.log_level_assert + ERROR -> R.color.log_level_error + } + companion object { @JvmStatic fun fromTag(tag: String): Level = when (tag) { diff --git a/app/src/main/java/com/nextcloud/client/logger/LogEntry.kt b/app/src/main/java/com/nextcloud/client/logger/LogEntry.kt index a0206da..6e47993 100644 --- a/app/src/main/java/com/nextcloud/client/logger/LogEntry.kt +++ b/app/src/main/java/com/nextcloud/client/logger/LogEntry.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.logger @@ -33,17 +33,15 @@ data class LogEntry(val timestamp: Date, val level: Level, val tag: String, val ) @JvmStatic - fun buildDateFormat(tz: TimeZone? = null): SimpleDateFormat { - return if (tz == null) { - SimpleDateFormat(UTC_DATE_FORMAT, Locale.US).apply { - timeZone = TIME_ZONE - isLenient = false - } - } else { - SimpleDateFormat(TZ_DATE_FORMAT, Locale.US).apply { - timeZone = tz - isLenient = false - } + fun buildDateFormat(tz: TimeZone? = null): SimpleDateFormat = if (tz == null) { + SimpleDateFormat(UTC_DATE_FORMAT, Locale.US).apply { + timeZone = TIME_ZONE + isLenient = false + } + } else { + SimpleDateFormat(TZ_DATE_FORMAT, Locale.US).apply { + timeZone = tz + isLenient = false } } diff --git a/app/src/main/java/com/nextcloud/client/logger/Logger.kt b/app/src/main/java/com/nextcloud/client/logger/Logger.kt index d7859ae..9a46105 100644 --- a/app/src/main/java/com/nextcloud/client/logger/Logger.kt +++ b/app/src/main/java/com/nextcloud/client/logger/Logger.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.logger diff --git a/app/src/main/java/com/nextcloud/client/logger/LoggerImpl.kt b/app/src/main/java/com/nextcloud/client/logger/LoggerImpl.kt index c09c47a..148000c 100644 --- a/app/src/main/java/com/nextcloud/client/logger/LoggerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/logger/LoggerImpl.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.logger @@ -22,7 +22,8 @@ internal class LoggerImpl( private val handler: FileLogHandler, private val mainThreadHandler: Handler, queueCapacity: Int -) : Logger, LogsRepository { +) : Logger, + LogsRepository { data class Load(val onResult: (List, Long) -> Unit) class Delete diff --git a/app/src/main/java/com/nextcloud/client/logger/LogsRepository.kt b/app/src/main/java/com/nextcloud/client/logger/LogsRepository.kt index b0871c7..30b4444 100644 --- a/app/src/main/java/com/nextcloud/client/logger/LogsRepository.kt +++ b/app/src/main/java/com/nextcloud/client/logger/LogsRepository.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.logger diff --git a/app/src/main/java/com/nextcloud/client/logger/ThreadLoop.kt b/app/src/main/java/com/nextcloud/client/logger/ThreadLoop.kt index 51b2cce..b56fe69 100644 --- a/app/src/main/java/com/nextcloud/client/logger/ThreadLoop.kt +++ b/app/src/main/java/com/nextcloud/client/logger/ThreadLoop.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.logger diff --git a/app/src/main/java/com/nextcloud/client/logger/ui/AsyncFilter.kt b/app/src/main/java/com/nextcloud/client/logger/ui/AsyncFilter.kt index 1c033ce..d42fc0f 100644 --- a/app/src/main/java/com/nextcloud/client/logger/ui/AsyncFilter.kt +++ b/app/src/main/java/com/nextcloud/client/logger/ui/AsyncFilter.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.logger.ui diff --git a/app/src/main/java/com/nextcloud/client/logger/ui/LogsActivity.kt b/app/src/main/java/com/nextcloud/client/logger/ui/LogsActivity.kt index 2e9dd85..214750c 100644 --- a/app/src/main/java/com/nextcloud/client/logger/ui/LogsActivity.kt +++ b/app/src/main/java/com/nextcloud/client/logger/ui/LogsActivity.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.logger.ui @@ -20,7 +20,6 @@ import com.nextcloud.client.di.ViewModelFactory import com.owncloud.android.R import com.owncloud.android.databinding.LogsActivityBinding import com.owncloud.android.ui.activity.ToolbarActivity -import com.owncloud.android.utils.theme.ViewThemeUtils import javax.inject.Inject class LogsActivity : ToolbarActivity() { @@ -28,17 +27,12 @@ class LogsActivity : ToolbarActivity() { @Inject lateinit var viewModelFactory: ViewModelFactory - @Inject - lateinit var viewThemeUtils: ViewThemeUtils - private lateinit var vm: LogsViewModel private lateinit var binding: LogsActivityBinding private lateinit var logsAdapter: LogsAdapter private val searchBoxListener = object : SearchView.OnQueryTextListener { - override fun onQueryTextSubmit(query: String): Boolean { - return false - } + override fun onQueryTextSubmit(query: String): Boolean = false override fun onQueryTextChange(newText: String): Boolean { vm.filter(newText) diff --git a/app/src/main/java/com/nextcloud/client/logger/ui/LogsAdapter.kt b/app/src/main/java/com/nextcloud/client/logger/ui/LogsAdapter.kt index 97141d5..35def37 100644 --- a/app/src/main/java/com/nextcloud/client/logger/ui/LogsAdapter.kt +++ b/app/src/main/java/com/nextcloud/client/logger/ui/LogsAdapter.kt @@ -2,34 +2,34 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.logger.ui +import android.annotation.SuppressLint import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView import com.nextcloud.client.logger.LogEntry import com.owncloud.android.R -import java.text.SimpleDateFormat -import java.util.Locale -class LogsAdapter(context: Context) : RecyclerView.Adapter() { +class LogsAdapter(private val context: Context) : RecyclerView.Adapter() { class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val header = view.findViewById(R.id.log_entry_list_item_header) - val message = view.findViewById(R.id.log_entry_list_item_message) + val header: TextView? = view.findViewById(R.id.log_entry_list_item_header) + val message: TextView? = view.findViewById(R.id.log_entry_list_item_message) } - private val timestampFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) private val inflater = LayoutInflater.from(context) var entries: List = listOf() + @SuppressLint("NotifyDataSetChanged") set(value) { - field = value + field = value.sortedBy { it.timestamp } notifyDataSetChanged() } @@ -39,10 +39,14 @@ class LogsAdapter(context: Context) : RecyclerView.Adapter - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.logger.ui @@ -86,12 +86,10 @@ class LogsEmailSender(private val context: Context, private val clock: Clock, pr } } - private fun getPhoneInfo(): String { - return "Model: " + Build.MODEL + "\n" + - "Brand: " + Build.BRAND + "\n" + - "Product: " + Build.PRODUCT + "\n" + - "Device: " + Build.DEVICE + "\n" + - "Version-Codename: " + Build.VERSION.CODENAME + "\n" + - "Version-Release: " + Build.VERSION.RELEASE - } + private fun getPhoneInfo(): String = "Model: " + Build.MODEL + "\n" + + "Brand: " + Build.BRAND + "\n" + + "Product: " + Build.PRODUCT + "\n" + + "Device: " + Build.DEVICE + "\n" + + "Version-Codename: " + Build.VERSION.CODENAME + "\n" + + "Version-Release: " + Build.VERSION.RELEASE } diff --git a/app/src/main/java/com/nextcloud/client/logger/ui/LogsViewModel.kt b/app/src/main/java/com/nextcloud/client/logger/ui/LogsViewModel.kt index b81ac22..46e4a8c 100644 --- a/app/src/main/java/com/nextcloud/client/logger/ui/LogsViewModel.kt +++ b/app/src/main/java/com/nextcloud/client/logger/ui/LogsViewModel.kt @@ -2,10 +2,11 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.logger.ui +import android.annotation.SuppressLint import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -17,6 +18,7 @@ import com.nextcloud.client.logger.LogsRepository import com.owncloud.android.R import javax.inject.Inject +@SuppressLint("StaticFieldLeak") class LogsViewModel @Inject constructor( private val context: Context, clock: Clock, diff --git a/app/src/main/java/com/nextcloud/client/media/AudioFocus.kt b/app/src/main/java/com/nextcloud/client/media/AudioFocus.kt index a151a75..c60702b 100644 --- a/app/src/main/java/com/nextcloud/client/media/AudioFocus.kt +++ b/app/src/main/java/com/nextcloud/client/media/AudioFocus.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.media diff --git a/app/src/main/java/com/nextcloud/client/media/AudioFocusManager.kt b/app/src/main/java/com/nextcloud/client/media/AudioFocusManager.kt index 2885092..358b393 100644 --- a/app/src/main/java/com/nextcloud/client/media/AudioFocusManager.kt +++ b/app/src/main/java/com/nextcloud/client/media/AudioFocusManager.kt @@ -2,13 +2,12 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.media import android.media.AudioFocusRequest import android.media.AudioManager -import android.os.Build /** * Wrapper around audio manager exposing simplified audio focus API and @@ -19,45 +18,29 @@ import android.os.Build */ internal class AudioFocusManager( private val audioManger: AudioManager, - private val onFocusChange: (AudioFocus) -> Unit + private val onFocusChange: (AudioFocus) -> Unit, + requestBuilder: AudioFocusRequest.Builder = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) ) { - - private val focusListener = object : AudioManager.OnAudioFocusChangeListener { - override fun onAudioFocusChange(focusChange: Int) { - val focus = when (focusChange) { - AudioManager.AUDIOFOCUS_GAIN -> AudioFocus.FOCUS - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT -> AudioFocus.FOCUS - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -> AudioFocus.FOCUS - AudioManager.AUDIOFOCUS_LOSS -> AudioFocus.LOST - AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> AudioFocus.LOST - AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> AudioFocus.DUCK - else -> null - } - focus?.let { onFocusChange(it) } - } - } - private var focusRequest: AudioFocusRequest? = null - - init { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run { - setWillPauseWhenDucked(true) - setOnAudioFocusChangeListener(focusListener) - }.build() + private val focusListener = AudioManager.OnAudioFocusChangeListener { focusChange -> + val focus = when (focusChange) { + AudioManager.AUDIOFOCUS_GAIN, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -> AudioFocus.FOCUS + AudioManager.AUDIOFOCUS_LOSS, + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> AudioFocus.LOST + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> AudioFocus.DUCK + else -> null } + focus?.let { onFocusChange(it) } } - /** - * Request audio focus. Focus is reported via callback. - * If focus cannot be gained, lost of focus is reported. - */ + private val focusRequest = requestBuilder + .setWillPauseWhenDucked(true) + .setOnAudioFocusChangeListener(focusListener) + .build() + fun requestFocus() { - val requestResult = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - focusRequest?.let { audioManger.requestAudioFocus(it) } - } else { - audioManger.requestAudioFocus(focusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN) - } - + val requestResult = audioManger.requestAudioFocus(focusRequest) if (requestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { focusListener.onAudioFocusChange(AudioManager.AUDIOFOCUS_GAIN) } else { @@ -65,17 +48,8 @@ internal class AudioFocusManager( } } - /** - * Release audio focus. Loss of focus is reported via callback. - */ fun releaseFocus() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - focusRequest?.let { - audioManger.abandonAudioFocusRequest(it) - } ?: AudioManager.AUDIOFOCUS_REQUEST_FAILED - } else { - audioManger.abandonAudioFocus(focusListener) - } + audioManger.abandonAudioFocusRequest(focusRequest) focusListener.onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS) } } diff --git a/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt b/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt new file mode 100644 index 0000000..7e1adca --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/media/BackgroundPlayerService.kt @@ -0,0 +1,241 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Parneet Singh + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.media + +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Bundle +import androidx.annotation.OptIn +import androidx.media3.common.Player +import androidx.media3.common.Player.COMMAND_PLAY_PAUSE +import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT +import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM +import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS +import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.session.CommandButton +import androidx.media3.session.DefaultMediaNotificationProvider +import androidx.media3.session.DefaultMediaNotificationProvider.COMMAND_KEY_COMPACT_VIEW_INDEX +import androidx.media3.session.MediaSession +import androidx.media3.session.MediaSession.ConnectionResult +import androidx.media3.session.MediaSession.ConnectionResult.AcceptedResultBuilder +import androidx.media3.session.MediaSessionService +import androidx.media3.session.SessionCommand +import androidx.media3.session.SessionResult +import com.google.common.collect.ImmutableList +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.di.Injectable +import com.nextcloud.client.media.NextcloudExoPlayer.createNextcloudExoplayer +import com.nextcloud.client.network.ClientFactory +import com.nextcloud.common.NextcloudClient +import com.nextcloud.utils.extensions.registerBroadcastReceiver +import com.owncloud.android.MainApp +import com.owncloud.android.datamodel.ReceiverFlag +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@OptIn(UnstableApi::class) +class BackgroundPlayerService : + MediaSessionService(), + Injectable { + + private val seekBackSessionCommand = SessionCommand(SESSION_COMMAND_ACTION_SEEK_BACK, Bundle.EMPTY) + private val seekForwardSessionCommand = SessionCommand(SESSION_COMMAND_ACTION_SEEK_FORWARD, Bundle.EMPTY) + + val seekForward = + CommandButton.Builder() + .setDisplayName("Seek Forward") + .setIconResId(CommandButton.getIconResIdForIconConstant(CommandButton.ICON_SKIP_FORWARD_15)) + .setSessionCommand(seekForwardSessionCommand) + .setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 2) }) + .build() + + val seekBackward = + CommandButton.Builder() + .setDisplayName("Seek Backward") + .setIconResId(CommandButton.getIconResIdForIconConstant(CommandButton.ICON_SKIP_BACK_5)) + .setSessionCommand(seekBackSessionCommand) + .setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 0) }) + .build() + + @Inject + lateinit var clientFactory: ClientFactory + + @Inject + lateinit var userAccountManager: UserAccountManager + lateinit var exoPlayer: ExoPlayer + private var mediaSession: MediaSession? = null + + private val stopReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + when (intent?.action) { + RELEASE_MEDIA_SESSION_BROADCAST_ACTION -> release() + STOP_MEDIA_SESSION_BROADCAST_ACTION -> exoPlayer.stop() + } + } + } + + override fun onCreate() { + super.onCreate() + + registerBroadcastReceiver( + stopReceiver, + IntentFilter().apply { + addAction(RELEASE_MEDIA_SESSION_BROADCAST_ACTION) + addAction(STOP_MEDIA_SESSION_BROADCAST_ACTION) + }, + ReceiverFlag.NotExported + ) + + MainApp.getAppComponent().inject(this) + initNextcloudExoPlayer() + + setMediaNotificationProvider(object : DefaultMediaNotificationProvider(this) { + override fun getMediaButtons( + session: MediaSession, + playerCommands: Player.Commands, + customLayout: ImmutableList, + showPauseButton: Boolean + ): ImmutableList { + val playPauseButton = + CommandButton.Builder() + .setDisplayName("PlayPause") + .setIconResId( + CommandButton.getIconResIdForIconConstant( + if (mediaSession?.player?.isPlaying == true) { + CommandButton.ICON_PAUSE + } else { + CommandButton.ICON_PLAY + } + ) + ) + .setPlayerCommand(COMMAND_PLAY_PAUSE) + .setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 1) }) + .build() + + val myCustomButtonsLayout = + ImmutableList.of(seekBackward, playPauseButton, seekForward) + return myCustomButtonsLayout + } + }) + } + + private fun initNextcloudExoPlayer() { + runBlocking { + var nextcloudClient: NextcloudClient + withContext(Dispatchers.IO) { + nextcloudClient = clientFactory.createNextcloudClient(userAccountManager.user) + } + nextcloudClient.let { + exoPlayer = createNextcloudExoplayer(this@BackgroundPlayerService, nextcloudClient) + mediaSession = + MediaSession.Builder(applicationContext, exoPlayer) + // set id to distinct this session to avoid crash + // in case session release delayed a bit and + // we start another session for eg. video + .setId(BACKGROUND_MEDIA_SESSION_ID) + .setCustomLayout(listOf(seekBackward, seekForward)) + .setCallback(object : MediaSession.Callback { + override fun onConnect( + session: MediaSession, + controller: MediaSession.ControllerInfo + ): ConnectionResult = AcceptedResultBuilder(mediaSession!!) + .setAvailablePlayerCommands( + ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon() + .remove(COMMAND_SEEK_TO_NEXT) + .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) + .remove(COMMAND_SEEK_TO_PREVIOUS) + .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) + .build() + ) + .setAvailableSessionCommands( + ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() + .addSessionCommands( + listOf(seekBackSessionCommand, seekForwardSessionCommand) + ).build() + ) + .build() + + override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) { + session.setCustomLayout(listOf(seekBackward, seekForward)) + } + + override fun onCustomCommand( + session: MediaSession, + controller: MediaSession.ControllerInfo, + customCommand: SessionCommand, + args: Bundle + ): ListenableFuture = when (customCommand.customAction) { + SESSION_COMMAND_ACTION_SEEK_FORWARD -> { + session.player.seekForward() + Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + + SESSION_COMMAND_ACTION_SEEK_BACK -> { + session.player.seekBack() + Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + + else -> super.onCustomCommand(session, controller, customCommand, args) + } + }) + .build() + } + } + } + + override fun onTaskRemoved(rootIntent: Intent?) { + release() + } + + override fun onDestroy() { + unregisterReceiver(stopReceiver) + mediaSession?.run { + player.release() + release() + mediaSession = null + } + super.onDestroy() + } + + private fun release() { + val player = mediaSession?.player + if (player?.playWhenReady == true) { + // Make sure the service is not in foreground. + player.pause() + } + // Bug in Android 14, https://github.com/androidx/media/issues/805 + // that sometimes onTaskRemove() doesn't get called immediately + // eventually gets called so the service stops but the notification doesn't clear out. + // [WORKAROUND] So, explicitly removing the notification here. + // TODO revisit after bug solved! + val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + nm.cancel(DefaultMediaNotificationProvider.DEFAULT_NOTIFICATION_ID) + stopSelf() + } + + override fun onGetSession(p0: MediaSession.ControllerInfo): MediaSession? = mediaSession + + companion object { + private const val SESSION_COMMAND_ACTION_SEEK_BACK = "SESSION_COMMAND_ACTION_SEEK_BACK" + private const val SESSION_COMMAND_ACTION_SEEK_FORWARD = "SESSION_COMMAND_ACTION_SEEK_FORWARD" + + private const val BACKGROUND_MEDIA_SESSION_ID = "com.nextcloud.client.media.BACKGROUND_MEDIA_SESSION_ID" + + const val RELEASE_MEDIA_SESSION_BROADCAST_ACTION = "com.nextcloud.client.media.RELEASE_MEDIA_SESSION" + const val STOP_MEDIA_SESSION_BROADCAST_ACTION = "com.nextcloud.client.media.STOP_MEDIA_SESSION" + } +} diff --git a/app/src/main/java/com/nextcloud/client/media/ErrorFormat.kt b/app/src/main/java/com/nextcloud/client/media/ErrorFormat.kt index 094a9b3..cb3cfec 100644 --- a/app/src/main/java/com/nextcloud/client/media/ErrorFormat.kt +++ b/app/src/main/java/com/nextcloud/client/media/ErrorFormat.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.media diff --git a/app/src/main/java/com/nextcloud/client/media/ExoplayerListener.kt b/app/src/main/java/com/nextcloud/client/media/ExoplayerListener.kt index 40de9de..3b0dd56 100644 --- a/app/src/main/java/com/nextcloud/client/media/ExoplayerListener.kt +++ b/app/src/main/java/com/nextcloud/client/media/ExoplayerListener.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.media @@ -22,8 +22,7 @@ class ExoplayerListener( private val playerView: View, private val exoPlayer: ExoPlayer, private val onCompleted: () -> Unit = { } -) : - Player.Listener { +) : Player.Listener { override fun onPlaybackStateChanged(playbackState: Int) { super.onPlaybackStateChanged(playbackState) diff --git a/app/src/main/java/com/nextcloud/client/media/LoadUrlTask.kt b/app/src/main/java/com/nextcloud/client/media/LoadUrlTask.kt index a5b7e8c..ea69556 100644 --- a/app/src/main/java/com/nextcloud/client/media/LoadUrlTask.kt +++ b/app/src/main/java/com/nextcloud/client/media/LoadUrlTask.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz * SPDX-FileCopyrightText: 2018 Tobias Kaminsky - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.media diff --git a/app/src/main/java/com/nextcloud/client/media/NextcloudExoPlayer.kt b/app/src/main/java/com/nextcloud/client/media/NextcloudExoPlayer.kt index 02b86ec..283f4e5 100644 --- a/app/src/main/java/com/nextcloud/client/media/NextcloudExoPlayer.kt +++ b/app/src/main/java/com/nextcloud/client/media/NextcloudExoPlayer.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.media diff --git a/app/src/main/java/com/nextcloud/client/media/Player.kt b/app/src/main/java/com/nextcloud/client/media/Player.kt index 1ba6918..0074690 100644 --- a/app/src/main/java/com/nextcloud/client/media/Player.kt +++ b/app/src/main/java/com/nextcloud/client/media/Player.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.media @@ -230,17 +230,11 @@ internal class Player( // region Media player controls - override fun isPlaying(): Boolean { - return stateMachine.isInState(State.PLAYING) - } + override fun isPlaying(): Boolean = stateMachine.isInState(State.PLAYING) - override fun canSeekForward(): Boolean { - return duration > MIN_DURATION_ALLOWING_SEEK - } + override fun canSeekForward(): Boolean = duration > MIN_DURATION_ALLOWING_SEEK - override fun canSeekBackward(): Boolean { - return duration > MIN_DURATION_ALLOWING_SEEK - } + override fun canSeekBackward(): Boolean = duration > MIN_DURATION_ALLOWING_SEEK override fun getDuration(): Int { val hasDuration = setOf(State.PLAYING, State.PAUSED) @@ -256,9 +250,7 @@ internal class Player( stateMachine.post(Event.PAUSE) } - override fun getBufferPercentage(): Int { - return 0 - } + override fun getBufferPercentage(): Int = 0 override fun seekTo(pos: Int) { if (stateMachine.isInState(State.PLAYING)) { @@ -266,21 +258,15 @@ internal class Player( } } - override fun getCurrentPosition(): Int { - return mediaPlayer?.currentPosition ?: 0 - } + override fun getCurrentPosition(): Int = mediaPlayer?.currentPosition ?: 0 override fun start() { stateMachine.post(Event.PLAY) } - override fun getAudioSessionId(): Int { - return 0 - } + override fun getAudioSessionId(): Int = 0 - override fun canPause(): Boolean { - return stateMachine.isInState(State.PLAYING) - } + override fun canPause(): Boolean = stateMachine.isInState(State.PLAYING) // endregion } diff --git a/app/src/main/java/com/nextcloud/client/media/PlayerError.kt b/app/src/main/java/com/nextcloud/client/media/PlayerError.kt index 78fc130..85a3808 100644 --- a/app/src/main/java/com/nextcloud/client/media/PlayerError.kt +++ b/app/src/main/java/com/nextcloud/client/media/PlayerError.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.media diff --git a/app/src/main/java/com/nextcloud/client/media/PlayerService.kt b/app/src/main/java/com/nextcloud/client/media/PlayerService.kt index 6e4be16..e7c67a7 100644 --- a/app/src/main/java/com/nextcloud/client/media/PlayerService.kt +++ b/app/src/main/java/com/nextcloud/client/media/PlayerService.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.media @@ -10,12 +10,12 @@ import android.app.PendingIntent import android.app.Service import android.content.Intent import android.media.AudioManager -import android.os.Build import android.os.Bundle import android.os.IBinder import android.widget.MediaController import android.widget.Toast import androidx.core.app.NotificationCompat +import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.nextcloud.client.account.User import com.nextcloud.client.network.ClientFactory import com.nextcloud.utils.ForegroundServiceHelper @@ -23,7 +23,9 @@ import com.nextcloud.utils.extensions.getParcelableArgument import com.owncloud.android.R import com.owncloud.android.datamodel.ForegroundServiceType import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.ui.notifications.NotificationUtils +import com.owncloud.android.ui.preview.PreviewMediaActivity import com.owncloud.android.utils.theme.ViewThemeUtils import dagger.android.AndroidInjection import java.util.Locale @@ -32,6 +34,8 @@ import javax.inject.Inject class PlayerService : Service() { companion object { + private const val TAG = "PlayerService" + const val EXTRA_USER = "USER" const val EXTRA_FILE = "FILE" const val EXTRA_AUTO_PLAY = "EXTRA_AUTO_PLAY" @@ -40,6 +44,8 @@ class PlayerService : Service() { const val ACTION_STOP = "STOP" const val ACTION_TOGGLE = "TOGGLE" const val ACTION_STOP_FILE = "STOP_FILE" + + const val IS_MEDIA_CONTROL_LAYOUT_READY = "IS_MEDIA_CONTROL_LAYOUT_READY" } class Binder(val service: PlayerService) : android.os.Binder() { @@ -52,24 +58,34 @@ class PlayerService : Service() { } private val playerListener = object : Player.Listener { - override fun onRunning(file: OCFile) { + Log_OC.d(TAG, "PlayerService.onRunning()") + val intent = Intent(PreviewMediaActivity.MEDIA_CONTROL_READY_RECEIVER).apply { + putExtra(IS_MEDIA_CONTROL_LAYOUT_READY, false) + } + LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent) startForeground(file) } override fun onStart() { - // empty + Log_OC.d(TAG, "PlayerService.onStart()") + val intent = Intent(PreviewMediaActivity.MEDIA_CONTROL_READY_RECEIVER).apply { + putExtra(IS_MEDIA_CONTROL_LAYOUT_READY, true) + } + LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent) } override fun onPause() { - // empty + Log_OC.d(TAG, "PlayerService.onPause()") } override fun onStop() { + Log_OC.d(TAG, "PlayerService.onStop()") stopServiceAndRemoveNotification(null) } override fun onError(error: PlayerError) { + Log_OC.d(TAG, "PlayerService.onError()") Toast.makeText(this@PlayerService, error.message, Toast.LENGTH_SHORT).show() } } @@ -89,29 +105,32 @@ class PlayerService : Service() { override fun onCreate() { super.onCreate() + AndroidInjection.inject(this) player = Player(applicationContext, clientFactory, playerListener, audioManager) notificationBuilder = NotificationCompat.Builder(this) viewThemeUtils.androidx.themeNotificationCompatBuilder(this, notificationBuilder) - val stop = Intent(this, PlayerService::class.java) - stop.action = ACTION_STOP - val pendingStop = PendingIntent.getService(this, 0, stop, PendingIntent.FLAG_IMMUTABLE) - notificationBuilder.addAction(0, getString(R.string.player_stop).toUpperCase(Locale.getDefault()), pendingStop) + val stop = Intent(this, PlayerService::class.java).apply { + action = ACTION_STOP + } + + val pendingStop = PendingIntent.getService(this, 0, stop, PendingIntent.FLAG_IMMUTABLE) + notificationBuilder.addAction(0, getString(R.string.player_stop).lowercase(Locale.getDefault()), pendingStop) + + val toggle = Intent(this, PlayerService::class.java).apply { + action = ACTION_TOGGLE + } - val toggle = Intent(this, PlayerService::class.java) - toggle.action = ACTION_TOGGLE val pendingToggle = PendingIntent.getService(this, 0, toggle, PendingIntent.FLAG_IMMUTABLE) notificationBuilder.addAction( 0, - getString(R.string.player_toggle).toUpperCase(Locale.getDefault()), + getString(R.string.player_toggle).lowercase(Locale.getDefault()), pendingToggle ) } - override fun onBind(intent: Intent?): IBinder? { - return Binder(this) - } + override fun onBind(intent: Intent?): IBinder? = Binder(this) override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { when (intent.action) { @@ -124,10 +143,12 @@ class PlayerService : Service() { } private fun onActionToggle() { - if (player.isPlaying) { - player.pause() - } else { - player.start() + player.run { + if (isPlaying) { + pause() + } else { + start() + } } } @@ -153,14 +174,14 @@ class PlayerService : Service() { private fun startForeground(currentFile: OCFile) { val ticker = String.format(getString(R.string.media_notif_ticker), getString(R.string.app_name)) val content = getString(R.string.media_state_playing, currentFile.getFileName()) - notificationBuilder.setSmallIcon(R.drawable.ic_play_arrow) - notificationBuilder.setWhen(System.currentTimeMillis()) - notificationBuilder.setOngoing(true) - notificationBuilder.setContentTitle(ticker) - notificationBuilder.setContentText(content) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - notificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_MEDIA) + notificationBuilder.run { + setSmallIcon(R.drawable.ic_play_arrow) + setWhen(System.currentTimeMillis()) + setOngoing(true) + setContentTitle(ticker) + setContentText(content) + setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_MEDIA) } ForegroundServiceHelper.startService( diff --git a/app/src/main/java/com/nextcloud/client/media/PlayerServiceConnection.kt b/app/src/main/java/com/nextcloud/client/media/PlayerServiceConnection.kt index 3d39cd6..2c89ca5 100644 --- a/app/src/main/java/com/nextcloud/client/media/PlayerServiceConnection.kt +++ b/app/src/main/java/com/nextcloud/client/media/PlayerServiceConnection.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.media @@ -10,9 +10,9 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection -import android.os.Build import android.os.IBinder import android.widget.MediaController +import androidx.core.content.ContextCompat import com.nextcloud.client.account.User import com.owncloud.android.datamodel.OCFile @@ -38,12 +38,14 @@ class PlayerServiceConnection(private val context: Context) : MediaController.Me } fun start(user: User, file: OCFile, playImmediately: Boolean, position: Long) { - val i = Intent(context, PlayerService::class.java) - i.putExtra(PlayerService.EXTRA_USER, user) - i.putExtra(PlayerService.EXTRA_FILE, file) - i.putExtra(PlayerService.EXTRA_AUTO_PLAY, playImmediately) - i.putExtra(PlayerService.EXTRA_START_POSITION_MS, position) - i.action = PlayerService.ACTION_PLAY + val i = Intent(context, PlayerService::class.java).apply { + putExtra(PlayerService.EXTRA_USER, user) + putExtra(PlayerService.EXTRA_FILE, file) + putExtra(PlayerService.EXTRA_AUTO_PLAY, playImmediately) + putExtra(PlayerService.EXTRA_START_POSITION_MS, position) + action = PlayerService.ACTION_PLAY + } + startForegroundService(i) } @@ -84,57 +86,37 @@ class PlayerServiceConnection(private val context: Context) : MediaController.Me // region Media controller - override fun isPlaying(): Boolean { - return binder?.player?.isPlaying ?: false - } + override fun isPlaying(): Boolean = binder?.player?.isPlaying ?: false - override fun canSeekForward(): Boolean { - return binder?.player?.canSeekForward() ?: false - } + override fun canSeekForward(): Boolean = binder?.player?.canSeekForward() ?: false - override fun getDuration(): Int { - return binder?.player?.duration ?: 0 - } + override fun getDuration(): Int = binder?.player?.duration ?: 0 override fun pause() { binder?.player?.pause() } - override fun getBufferPercentage(): Int { - return binder?.player?.bufferPercentage ?: 0 - } + override fun getBufferPercentage(): Int = binder?.player?.bufferPercentage ?: 0 override fun seekTo(pos: Int) { binder?.player?.seekTo(pos) } - override fun getCurrentPosition(): Int { - return binder?.player?.currentPosition ?: 0 - } + override fun getCurrentPosition(): Int = binder?.player?.currentPosition ?: 0 - override fun canSeekBackward(): Boolean { - return binder?.player?.canSeekBackward() ?: false - } + override fun canSeekBackward(): Boolean = binder?.player?.canSeekBackward() ?: false override fun start() { binder?.player?.start() } - override fun getAudioSessionId(): Int { - return 0 - } + override fun getAudioSessionId(): Int = 0 - override fun canPause(): Boolean { - return binder?.player?.canPause() ?: false - } + override fun canPause(): Boolean = binder?.player?.canPause() ?: false // endregion private fun startForegroundService(i: Intent) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(i) - } else { - context.startService(i) - } + ContextCompat.startForegroundService(context, i) } } diff --git a/app/src/main/java/com/nextcloud/client/media/PlayerStateMachine.kt b/app/src/main/java/com/nextcloud/client/media/PlayerStateMachine.kt index 1fcfcb1..3310244 100644 --- a/app/src/main/java/com/nextcloud/client/media/PlayerStateMachine.kt +++ b/app/src/main/java/com/nextcloud/client/media/PlayerStateMachine.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.media @@ -191,9 +191,7 @@ internal class PlayerStateMachine(initialState: State, private val delegate: Del * Contrary to [PlayerStateMachine.state] attribute, this method checks for * parent states. */ - fun isInState(state: State): Boolean { - return stateMachine.isInState(state) - } + fun isInState(state: State): Boolean = stateMachine.isInState(state) /** * Post state machine event to internal queue. diff --git a/app/src/main/java/com/nextcloud/client/media/PlaylistItem.kt b/app/src/main/java/com/nextcloud/client/media/PlaylistItem.kt index 616d2d1..2415145 100644 --- a/app/src/main/java/com/nextcloud/client/media/PlaylistItem.kt +++ b/app/src/main/java/com/nextcloud/client/media/PlaylistItem.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2021 Tobias Kaminsky * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.media diff --git a/app/src/main/java/com/nextcloud/client/migrations/MigrationError.kt b/app/src/main/java/com/nextcloud/client/migrations/MigrationError.kt index 67aabec..b0c15c9 100644 --- a/app/src/main/java/com/nextcloud/client/migrations/MigrationError.kt +++ b/app/src/main/java/com/nextcloud/client/migrations/MigrationError.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.migrations diff --git a/app/src/main/java/com/nextcloud/client/migrations/MigrationInfo.kt b/app/src/main/java/com/nextcloud/client/migrations/MigrationInfo.kt index 2e22728..135c7b9 100644 --- a/app/src/main/java/com/nextcloud/client/migrations/MigrationInfo.kt +++ b/app/src/main/java/com/nextcloud/client/migrations/MigrationInfo.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.migrations diff --git a/app/src/main/java/com/nextcloud/client/migrations/Migrations.kt b/app/src/main/java/com/nextcloud/client/migrations/Migrations.kt index 31d4a0e..2c3724c 100644 --- a/app/src/main/java/com/nextcloud/client/migrations/Migrations.kt +++ b/app/src/main/java/com/nextcloud/client/migrations/Migrations.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.migrations @@ -42,9 +42,7 @@ class Migrations @Inject constructor( * into [MigrationException] */ class Step(val id: Int, val description: String, val mandatory: Boolean = true, val run: (s: Step) -> Unit) { - override fun toString(): String { - return "Migration $id: $description" - } + override fun toString(): String = "Migration $id: $description" } /** diff --git a/app/src/main/java/com/nextcloud/client/migrations/MigrationsDb.kt b/app/src/main/java/com/nextcloud/client/migrations/MigrationsDb.kt index d7e0357..235f3cf 100644 --- a/app/src/main/java/com/nextcloud/client/migrations/MigrationsDb.kt +++ b/app/src/main/java/com/nextcloud/client/migrations/MigrationsDb.kt @@ -2,11 +2,12 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.migrations import android.content.SharedPreferences +import androidx.core.content.edit import java.util.TreeSet class MigrationsDb(private val migrationsDb: SharedPreferences) { @@ -39,12 +40,12 @@ class MigrationsDb(private val migrationsDb: SharedPreferences) { addAll(oldApplied) addAll(migrations.map { it.toString() }) } - migrationsDb.edit().putStringSet(DB_KEY_APPLIED_MIGRATIONS, newApplied).apply() + migrationsDb.edit { putStringSet(DB_KEY_APPLIED_MIGRATIONS, newApplied) } } var lastMigratedVersion: Int set(value) { - migrationsDb.edit().putInt(DB_KEY_LAST_MIGRATED_VERSION, value).apply() + migrationsDb.edit { putInt(DB_KEY_LAST_MIGRATED_VERSION, value) } } get() { return migrationsDb.getInt(DB_KEY_LAST_MIGRATED_VERSION, NO_LAST_MIGRATED_VERSION) @@ -56,17 +57,17 @@ class MigrationsDb(private val migrationsDb: SharedPreferences) { fun setFailed(id: Int, error: String) { migrationsDb - .edit() - .putBoolean(DB_KEY_FAILED, true) - .putString(DB_KEY_FAILED_MIGRATION_ERROR_MESSAGE, error) - .putInt(DB_KEY_FAILED_MIGRATION_ID, id) - .apply() + .edit { + putBoolean(DB_KEY_FAILED, true) + .putString(DB_KEY_FAILED_MIGRATION_ERROR_MESSAGE, error) + .putInt(DB_KEY_FAILED_MIGRATION_ID, id) + } } fun clearMigrations() { - migrationsDb.edit() - .putStringSet(DB_KEY_APPLIED_MIGRATIONS, emptySet()) - .putInt(DB_KEY_LAST_MIGRATED_VERSION, 0) - .apply() + migrationsDb.edit { + putStringSet(DB_KEY_APPLIED_MIGRATIONS, emptySet()) + .putInt(DB_KEY_LAST_MIGRATED_VERSION, 0) + } } } diff --git a/app/src/main/java/com/nextcloud/client/migrations/MigrationsManager.kt b/app/src/main/java/com/nextcloud/client/migrations/MigrationsManager.kt index 5fc6b82..730762b 100644 --- a/app/src/main/java/com/nextcloud/client/migrations/MigrationsManager.kt +++ b/app/src/main/java/com/nextcloud/client/migrations/MigrationsManager.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.migrations diff --git a/app/src/main/java/com/nextcloud/client/migrations/MigrationsManagerImpl.kt b/app/src/main/java/com/nextcloud/client/migrations/MigrationsManagerImpl.kt index d6afa00..eb7a7cc 100644 --- a/app/src/main/java/com/nextcloud/client/migrations/MigrationsManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/migrations/MigrationsManagerImpl.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.migrations diff --git a/app/src/main/java/com/nextcloud/client/mixins/ActivityMixin.kt b/app/src/main/java/com/nextcloud/client/mixins/ActivityMixin.kt index 98406e2..6176146 100644 --- a/app/src/main/java/com/nextcloud/client/mixins/ActivityMixin.kt +++ b/app/src/main/java/com/nextcloud/client/mixins/ActivityMixin.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.mixins diff --git a/app/src/main/java/com/nextcloud/client/mixins/MixinRegistry.kt b/app/src/main/java/com/nextcloud/client/mixins/MixinRegistry.kt index c38e066..66a5446 100644 --- a/app/src/main/java/com/nextcloud/client/mixins/MixinRegistry.kt +++ b/app/src/main/java/com/nextcloud/client/mixins/MixinRegistry.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.mixins diff --git a/app/src/main/java/com/nextcloud/client/mixins/SessionMixin.kt b/app/src/main/java/com/nextcloud/client/mixins/SessionMixin.kt index 58bbcc4..1fff6f6 100644 --- a/app/src/main/java/com/nextcloud/client/mixins/SessionMixin.kt +++ b/app/src/main/java/com/nextcloud/client/mixins/SessionMixin.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.mixins @@ -25,10 +25,7 @@ import java.util.Optional * It is an intermediary step facilitating comprehensive rework of * account handling logic. */ -class SessionMixin( - private val activity: Activity, - private val accountManager: UserAccountManager -) : ActivityMixin { +class SessionMixin(private val activity: Activity, private val accountManager: UserAccountManager) : ActivityMixin { var currentAccount: Account = getDefaultAccount() private set @@ -51,12 +48,10 @@ class SessionMixin( setAccount(user.toPlatformAccount()) } - fun getUser(): Optional { - return if (currentAccount.isAnonymous(activity)) { - Optional.empty() - } else { - accountManager.getUser(currentAccount.name) - } + fun getUser(): Optional = if (currentAccount.isAnonymous(activity)) { + Optional.empty() + } else { + accountManager.getUser(currentAccount.name) } /** diff --git a/app/src/main/java/com/nextcloud/client/network/ClientFactory.java b/app/src/main/java/com/nextcloud/client/network/ClientFactory.java index 47770f7..a0780bd 100644 --- a/app/src/main/java/com/nextcloud/client/network/ClientFactory.java +++ b/app/src/main/java/com/nextcloud/client/network/ClientFactory.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.network; diff --git a/app/src/main/java/com/nextcloud/client/network/ClientFactoryImpl.java b/app/src/main/java/com/nextcloud/client/network/ClientFactoryImpl.java index e753f98..22fe0c8 100644 --- a/app/src/main/java/com/nextcloud/client/network/ClientFactoryImpl.java +++ b/app/src/main/java/com/nextcloud/client/network/ClientFactoryImpl.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.network; diff --git a/app/src/main/java/com/nextcloud/client/network/Connectivity.kt b/app/src/main/java/com/nextcloud/client/network/Connectivity.kt index 11ac986..e3b4b19 100644 --- a/app/src/main/java/com/nextcloud/client/network/Connectivity.kt +++ b/app/src/main/java/com/nextcloud/client/network/Connectivity.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.network diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java b/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java index 2f97bae..7da4afe 100644 --- a/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java +++ b/app/src/main/java/com/nextcloud/client/network/ConnectivityService.java @@ -2,15 +2,29 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.network; + +import androidx.annotation.NonNull; + /** * This service provides information about current network connectivity * and server reachability. */ public interface ConnectivityService { + /** + * Checks the availability of the server and the device's internet connection. + *

+ * This method performs a network request to verify if the server is accessible and + * checks if the device has an active internet connection. + *

+ * + * @param callback A callback to handle the result of the network and server availability check. + */ + void isNetworkAndServerAvailable(@NonNull GenericCallback callback); + boolean isConnected(); /** @@ -28,4 +42,13 @@ public interface ConnectivityService { * @return Network connectivity status in platform-agnostic format */ Connectivity getConnectivity(); + + /** + * Callback interface for asynchronous results. + * + * @param The type of result returned by the callback. + */ + interface GenericCallback { + void onComplete(T result); + } } diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java index f1582fb..713cea7 100644 --- a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java +++ b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.java @@ -4,7 +4,7 @@ * @author Chris Narkiewicz * Copyright (C) 2021 Chris Narkiewicz * - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.network; @@ -13,6 +13,9 @@ import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkInfo; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; import com.nextcloud.client.account.Server; import com.nextcloud.client.account.UserAccountManager; @@ -22,6 +25,7 @@ import com.owncloud.android.lib.common.utils.Log_OC; import org.apache.commons.httpclient.HttpStatus; +import androidx.annotation.NonNull; import androidx.core.net.ConnectivityManagerCompat; import kotlin.jvm.functions.Function1; @@ -35,6 +39,7 @@ class ConnectivityServiceImpl implements ConnectivityService { private final ClientFactory clientFactory; private final GetRequestBuilder requestBuilder; private final WalledCheckCache walledCheckCache; + private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); static class GetRequestBuilder implements Function1 { @Override @@ -55,6 +60,24 @@ class ConnectivityServiceImpl implements ConnectivityService { this.walledCheckCache = walledCheckCache; } + @Override + public void isNetworkAndServerAvailable(@NonNull GenericCallback callback) { + new Thread(() -> { + Network activeNetwork = platformConnectivityManager.getActiveNetwork(); + NetworkCapabilities networkCapabilities = platformConnectivityManager.getNetworkCapabilities(activeNetwork); + boolean hasInternet = networkCapabilities != null && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + + boolean result; + if (hasInternet) { + result = !isInternetWalled(); + } else { + result = false; + } + + mainThreadHandler.post(() -> callback.onComplete(result)); + }).start(); + } + @Override public boolean isConnected() { Network nw = platformConnectivityManager.getActiveNetwork(); @@ -64,10 +87,21 @@ class ConnectivityServiceImpl implements ConnectivityService { return false; } - return actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || + if (actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) || - actNw.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH); + actNw.hasTransport(NetworkCapabilities.TRANSPORT_VPN) || + actNw.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) || + actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI_AWARE)) { + return true; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + actNw.hasTransport(NetworkCapabilities.TRANSPORT_USB)) { + return true; + } + + return false; } @Override @@ -76,31 +110,26 @@ class ConnectivityServiceImpl implements ConnectivityService { if (cachedValue != null) { return cachedValue; } else { + Server server = accountManager.getUser().getServer(); + String baseServerAddress = server.getUri().toString(); + boolean result; Connectivity c = getConnectivity(); - if (c.isConnected() && c.isWifi() && !c.isMetered()) { + if (c != null && c.isConnected() && c.isWifi() && !c.isMetered() && !baseServerAddress.isEmpty()) { + GetMethod get = requestBuilder.invoke(baseServerAddress + CONNECTIVITY_CHECK_ROUTE); + PlainClient client = clientFactory.createPlainClient(); - Server server = accountManager.getUser().getServer(); - String baseServerAddress = server.getUri().toString(); - if (baseServerAddress.isEmpty()) { - result = true; - } else { + int status = get.execute(client); - GetMethod get = requestBuilder.invoke(baseServerAddress + CONNECTIVITY_CHECK_ROUTE); - PlainClient client = clientFactory.createPlainClient(); - - int status = get.execute(client); - - // Content-Length is not available when using chunked transfer encoding, so check for -1 as well - result = !(status == HttpStatus.SC_NO_CONTENT && get.getResponseContentLength() <= 0); - get.releaseConnection(); - if (result) { - Log_OC.w(TAG, "isInternetWalled(): Failed to GET " + CONNECTIVITY_CHECK_ROUTE + "," + - " assuming connectivity is impaired"); - } + // Content-Length is not available when using chunked transfer encoding, so check for -1 as well + result = !(status == HttpStatus.SC_NO_CONTENT && get.getResponseContentLength() <= 0); + get.releaseConnection(); + if (result) { + Log_OC.w(TAG, "isInternetWalled(): Failed to GET " + CONNECTIVITY_CHECK_ROUTE + "," + + " assuming connectivity is impaired"); } } else { - result = !c.isConnected(); + result = (c != null && !c.isConnected()); } walledCheckCache.setValue(result); diff --git a/app/src/main/java/com/nextcloud/client/network/NetworkModule.java b/app/src/main/java/com/nextcloud/client/network/NetworkModule.java index f0b9b9a..8fbca7e 100644 --- a/app/src/main/java/com/nextcloud/client/network/NetworkModule.java +++ b/app/src/main/java/com/nextcloud/client/network/NetworkModule.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.network; diff --git a/app/src/main/java/com/nextcloud/client/network/WalledCheckCache.kt b/app/src/main/java/com/nextcloud/client/network/WalledCheckCache.kt index cac5b40..7246732 100644 --- a/app/src/main/java/com/nextcloud/client/network/WalledCheckCache.kt +++ b/app/src/main/java/com/nextcloud/client/network/WalledCheckCache.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.network @@ -17,13 +17,11 @@ class WalledCheckCache @Inject constructor(private val clock: Clock) { private var cachedEntry: Pair? = null @Synchronized - fun isExpired(): Boolean { - return when (val timestamp = cachedEntry?.first) { - null -> true - else -> { - val diff = clock.currentTime - timestamp - diff >= CACHE_TIME_MS - } + fun isExpired(): Boolean = when (val timestamp = cachedEntry?.first) { + null -> true + else -> { + val diff = clock.currentTime - timestamp + diff >= CACHE_TIME_MS } } @@ -33,11 +31,9 @@ class WalledCheckCache @Inject constructor(private val clock: Clock) { } @Synchronized - fun getValue(): Boolean? { - return when (isExpired()) { - true -> null - else -> cachedEntry?.second - } + fun getValue(): Boolean? = when (isExpired()) { + true -> null + else -> cachedEntry?.second } @Synchronized diff --git a/app/src/main/java/com/nextcloud/client/notifications/AppNotificationManager.kt b/app/src/main/java/com/nextcloud/client/notifications/AppNotificationManager.kt index 7c99423..6ee8e6e 100644 --- a/app/src/main/java/com/nextcloud/client/notifications/AppNotificationManager.kt +++ b/app/src/main/java/com/nextcloud/client/notifications/AppNotificationManager.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020-2021 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.notifications diff --git a/app/src/main/java/com/nextcloud/client/notifications/AppNotificationManagerImpl.kt b/app/src/main/java/com/nextcloud/client/notifications/AppNotificationManagerImpl.kt index 78f31f8..2aed1d2 100644 --- a/app/src/main/java/com/nextcloud/client/notifications/AppNotificationManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/notifications/AppNotificationManagerImpl.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020-2021 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.notifications @@ -13,7 +13,6 @@ import android.content.Context import android.content.Intent import android.content.res.Resources import android.graphics.BitmapFactory -import android.os.Build import androidx.core.app.NotificationCompat import com.nextcloud.client.account.User import com.owncloud.android.R @@ -38,11 +37,8 @@ class AppNotificationManagerImpl @Inject constructor( } private fun builder(channelId: String): NotificationCompat.Builder { - val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val builder = NotificationCompat.Builder(context, channelId) - } else { - NotificationCompat.Builder(context) - } viewThemeUtils.androidx.themeNotificationCompatBuilder(context, builder) return builder } diff --git a/app/src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.kt b/app/src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.kt index 6c80514..824e7b2 100644 --- a/app/src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.kt +++ b/app/src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2018 Tobias Kaminsky - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.onboarding @@ -18,12 +18,13 @@ import androidx.activity.OnBackPressedCallback import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts -import androidx.viewpager.widget.ViewPager +import androidx.viewpager2.widget.ViewPager2 import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.appinfo.AppInfo import com.nextcloud.client.di.Injectable import com.nextcloud.client.preferences.AppPreferences +import com.nextcloud.utils.mdm.MDMConfig import com.owncloud.android.BuildConfig import com.owncloud.android.R import com.owncloud.android.authentication.AuthenticatorActivity @@ -39,7 +40,9 @@ import javax.inject.Inject /** * Activity displaying general feature after a fresh install. */ -class FirstRunActivity : BaseActivity(), ViewPager.OnPageChangeListener, Injectable { +class FirstRunActivity : + BaseActivity(), + Injectable { @JvmField @Inject @@ -76,13 +79,12 @@ class FirstRunActivity : BaseActivity(), ViewPager.OnPageChangeListener, Injecta binding = FirstRunActivityBinding.inflate(layoutInflater) setContentView(binding.root) - val isProviderOrOwnInstallationVisible = resources.getBoolean(R.bool.show_provider_or_own_installation) setSlideshowSize(resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) registerActivityResult() setupLoginButton() - setupSignupButton(isProviderOrOwnInstallationVisible) - setupHostOwnServerTextView(isProviderOrOwnInstallationVisible) + setupSignupButton(MDMConfig.showIntro(this)) + setupHostOwnServerTextView(MDMConfig.showIntro(this)) deleteAccountAtFirstLaunch() setupFeaturesViewAdapter() handleOnBackPressed() @@ -90,7 +92,7 @@ class FirstRunActivity : BaseActivity(), ViewPager.OnPageChangeListener, Injecta private fun applyDefaultTheme() { defaultViewThemeUtils = viewThemeUtilsFactory?.withPrimaryAsBackground() - defaultViewThemeUtils?.platform?.themeStatusBar(this, ColorRole.PRIMARY) + defaultViewThemeUtils?.platform?.colorStatusBar(this, resources.getColor(R.color.primary)) } private fun registerActivityResult() { @@ -171,10 +173,14 @@ class FirstRunActivity : BaseActivity(), ViewPager.OnPageChangeListener, Injecta @Suppress("SpreadOperator") private fun setupFeaturesViewAdapter() { - val featuresViewAdapter = FeaturesViewAdapter(supportFragmentManager, *firstRun) - binding.progressIndicator.setNumberOfSteps(featuresViewAdapter.count) + val featuresViewAdapter = FeaturesViewAdapter(this, *firstRun) + binding.progressIndicator.setNumberOfSteps(featuresViewAdapter.itemCount) binding.contentPanel.adapter = featuresViewAdapter - binding.contentPanel.addOnPageChangeListener(this) + binding.contentPanel.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + binding.progressIndicator.animateToStep(position + 1) + } + }) } private fun handleOnBackPressed() { @@ -203,10 +209,9 @@ class FirstRunActivity : BaseActivity(), ViewPager.OnPageChangeListener, Injecta } private fun setSlideshowSize(isLandscape: Boolean) { - val isProviderOrOwnInstallationVisible = resources.getBoolean(R.bool.show_provider_or_own_installation) binding.buttonLayout.orientation = if (isLandscape) LinearLayout.HORIZONTAL else LinearLayout.VERTICAL - val layoutParams: LinearLayout.LayoutParams = if (isProviderOrOwnInstallationVisible) { + val layoutParams: LinearLayout.LayoutParams = if (MDMConfig.showIntro(this)) { LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT @@ -236,18 +241,6 @@ class FirstRunActivity : BaseActivity(), ViewPager.OnPageChangeListener, Injecta super.onStop() } - override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { - // unused but to be implemented due to abstract parent - } - - override fun onPageSelected(position: Int) { - binding.progressIndicator.animateToStep(position + 1) - } - - override fun onPageScrollStateChanged(state: Int) { - // unused but to be implemented due to abstract parent - } - companion object { const val EXTRA_ALLOW_CLOSE = "ALLOW_CLOSE" const val EXTRA_EXIT = "EXIT" diff --git a/app/src/main/java/com/nextcloud/client/onboarding/OnboardingModule.kt b/app/src/main/java/com/nextcloud/client/onboarding/OnboardingModule.kt index e6e6fe6..b97382f 100644 --- a/app/src/main/java/com/nextcloud/client/onboarding/OnboardingModule.kt +++ b/app/src/main/java/com/nextcloud/client/onboarding/OnboardingModule.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.onboarding @@ -22,7 +22,5 @@ class OnboardingModule { resources: Resources, preferences: AppPreferences, accountProvider: CurrentAccountProvider - ): OnboardingService { - return OnboardingServiceImpl(resources, preferences, accountProvider) - } + ): OnboardingService = OnboardingServiceImpl(resources, preferences, accountProvider) } diff --git a/app/src/main/java/com/nextcloud/client/onboarding/OnboardingService.kt b/app/src/main/java/com/nextcloud/client/onboarding/OnboardingService.kt index 7332c56..e75955f 100644 --- a/app/src/main/java/com/nextcloud/client/onboarding/OnboardingService.kt +++ b/app/src/main/java/com/nextcloud/client/onboarding/OnboardingService.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.onboarding diff --git a/app/src/main/java/com/nextcloud/client/onboarding/OnboardingServiceImpl.kt b/app/src/main/java/com/nextcloud/client/onboarding/OnboardingServiceImpl.kt index 129da4b..ceaf911 100644 --- a/app/src/main/java/com/nextcloud/client/onboarding/OnboardingServiceImpl.kt +++ b/app/src/main/java/com/nextcloud/client/onboarding/OnboardingServiceImpl.kt @@ -2,7 +2,8 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2024 TSI-mc + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.onboarding @@ -12,13 +13,14 @@ import android.content.Intent import android.content.res.Resources import com.nextcloud.client.account.CurrentAccountProvider import com.nextcloud.client.preferences.AppPreferences +import com.nextcloud.utils.mdm.MDMConfig import com.owncloud.android.BuildConfig import com.owncloud.android.R import com.owncloud.android.authentication.AuthenticatorActivity import com.owncloud.android.features.FeatureItem import com.owncloud.android.ui.activity.PassCodeActivity -internal class OnboardingServiceImpl constructor( +internal class OnboardingServiceImpl( private val resources: Resources, private val preferences: AppPreferences, private val accountProvider: CurrentAccountProvider @@ -42,12 +44,11 @@ internal class OnboardingServiceImpl constructor( override val isFirstRun: Boolean get() { - return accountProvider.currentAccount == null + return accountProvider.user.isAnonymous } - override fun shouldShowWhatsNew(callingContext: Context): Boolean { - return callingContext !is PassCodeActivity && whatsNew.isNotEmpty() - } + override fun shouldShowWhatsNew(callingContext: Context): Boolean = + callingContext !is PassCodeActivity && whatsNew.isNotEmpty() override fun launchActivityIfNeeded(activity: Activity) { if (!resources.getBoolean(R.bool.show_whats_new) || activity is WhatsNewActivity) { @@ -60,8 +61,7 @@ internal class OnboardingServiceImpl constructor( } override fun launchFirstRunIfNeeded(activity: Activity): Boolean { - val isProviderOrOwnInstallationVisible = resources.getBoolean(R.bool.show_provider_or_own_installation) - val canLaunch = isProviderOrOwnInstallationVisible && isFirstRun && activity is AuthenticatorActivity + val canLaunch = MDMConfig.showIntro(activity) && isFirstRun && activity is AuthenticatorActivity if (canLaunch) { val intent = Intent(activity, FirstRunActivity::class.java) activity.startActivityForResult(intent, AuthenticatorActivity.REQUEST_CODE_FIRST_RUN) diff --git a/app/src/main/java/com/nextcloud/client/onboarding/WhatsNewActivity.kt b/app/src/main/java/com/nextcloud/client/onboarding/WhatsNewActivity.kt index f62168e..dd51319 100644 --- a/app/src/main/java/com/nextcloud/client/onboarding/WhatsNewActivity.kt +++ b/app/src/main/java/com/nextcloud/client/onboarding/WhatsNewActivity.kt @@ -1,11 +1,11 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Álvaro Brey * SPDX-FileCopyrightText: 2017 Tobias Kaminsky * SPDX-FileCopyrightText: 2016 Andy Scherzinger - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.onboarding @@ -13,7 +13,7 @@ import android.os.Bundle import android.view.View import androidx.activity.OnBackPressedCallback import androidx.fragment.app.FragmentActivity -import androidx.viewpager.widget.ViewPager +import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.appinfo.AppInfo import com.nextcloud.client.di.Injectable @@ -29,7 +29,9 @@ import javax.inject.Inject /** * Activity displaying new features after an update. */ -class WhatsNewActivity : FragmentActivity(), ViewPager.OnPageChangeListener, Injectable { +class WhatsNewActivity : + FragmentActivity(), + Injectable { @JvmField @Inject @@ -64,7 +66,11 @@ class WhatsNewActivity : FragmentActivity(), ViewPager.OnPageChangeListener, Inj val showWebView = urls.isNotEmpty() setupFeatureViewAdapter(showWebView, urls) - binding.contentPanel.addOnPageChangeListener(this) + binding.contentPanel.registerOnPageChangeCallback(object : OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + controlPanelOnPageSelected(position) + } + }) setupForwardImageButton() setupSkipImageButton() setupWelcomeText(showWebView) @@ -75,15 +81,15 @@ class WhatsNewActivity : FragmentActivity(), ViewPager.OnPageChangeListener, Inj @Suppress("SpreadOperator") private fun setupFeatureViewAdapter(showWebView: Boolean, urls: Array) { val adapter = if (showWebView) { - FeaturesWebViewAdapter(supportFragmentManager, *urls) + FeaturesWebViewAdapter(this, *urls) } else { onboarding?.let { - FeaturesViewAdapter(supportFragmentManager, *it.whatsNew) + FeaturesViewAdapter(this, *it.whatsNew) } } adapter?.let { - binding.progressIndicator.setNumberOfSteps(it.count) + binding.progressIndicator.setNumberOfSteps(it.itemCount) binding.contentPanel.adapter = it } } @@ -126,6 +132,7 @@ class WhatsNewActivity : FragmentActivity(), ViewPager.OnPageChangeListener, Inj object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { onFinish() + isEnabled = false onBackPressedDispatcher.onBackPressed() } } @@ -142,14 +149,8 @@ class WhatsNewActivity : FragmentActivity(), ViewPager.OnPageChangeListener, Inj preferences?.lastSeenVersionCode = BuildConfig.VERSION_CODE } - override fun onPageSelected(position: Int) { + private fun controlPanelOnPageSelected(position: Int) { binding.progressIndicator.animateToStep(position + 1) updateNextButtonIfNeeded() } - - @Suppress("EmptyFunctionBlock") - override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} - - @Suppress("EmptyFunctionBlock") - override fun onPageScrollStateChanged(state: Int) {} } diff --git a/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java index b2517b5..cc64609 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java +++ b/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java @@ -63,6 +63,12 @@ public interface AppPreferences { boolean isShowHiddenFilesEnabled(); void setShowHiddenFilesEnabled(boolean enabled); + + boolean isSortFoldersBeforeFiles(); + void setSortFoldersBeforeFiles(boolean enabled); + + boolean isSortFavoritesFirst(); + void setSortFavoritesFirst(boolean enabled); boolean isShowEcosystemApps(); void setShowEcosystemApps(boolean enabled); @@ -167,14 +173,6 @@ public interface AppPreferences { */ String[] getPassCode(); - /** - * Gets the unlock via fingerprint preference configured by the user. - * - * @implNote this is always false - * @return useFingerprint is unlock with fingerprint enabled - */ - boolean isFingerprintUnlockEnabled(); - /** * Gets the auto upload paths flag last set. * @@ -210,7 +208,7 @@ public interface AppPreferences { * Get preferred folder sort order. * * @param folder Folder whoch order is being retrieved or null for root folder - * @return sort order the sort order, default is {@link FileSortOrder#sort_a_to_z} (sort by name) + * @return sort order the sort order, default is {@link FileSortOrder# sort_a_to_z} (sort by name) */ FileSortOrder getSortOrderByFolder(@Nullable OCFile folder); @@ -232,7 +230,7 @@ public interface AppPreferences { /** * Get preferred folder sort order. * - * @return sort order the sort order, default is {@link FileSortOrder#sort_a_to_z} (sort by name) + * @return sort order the sort order, default is {@link FileSortOrder# sort_a_to_z} (sort by name) */ FileSortOrder getSortOrderByType(FileSortOrder.Type type, FileSortOrder defaultOrder); FileSortOrder getSortOrderByType(FileSortOrder.Type type); @@ -352,10 +350,6 @@ public interface AppPreferences { long getPhotoSearchTimestamp(); - boolean isPowerCheckDisabled(); - - void setPowerCheckDisabled(boolean value); - void increasePinWrongAttempts(); void resetPinWrongAttempts(); @@ -391,4 +385,19 @@ public interface AppPreferences { @NonNull String getLastSelectedMediaFolder(); + + void setTwoWaySyncStatus(boolean value); + boolean isTwoWaySyncEnabled(); + + void setTwoWaySyncInterval(Long value); + Long getTwoWaySyncInterval(); + + boolean shouldStopDownloadJobsOnStart(); + void setStopDownloadJobsOnStart(boolean value); + + int getPassCodeDelay(); + void setPassCodeDelay(int value); + + String getLastDisplayedAccountName(); + void setLastDisplayedAccountName(String lastDisplayedAccountName); } diff --git a/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java b/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java index 4531fd2..f6ebc7d 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java +++ b/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java @@ -6,7 +6,7 @@ * SPDX-FileCopyrightText: 2019 Tobias Kaminsky * SPDX-FileCopyrightText: 2019 Chris Narkiewicz * SPDX-FileCopyrightText: 2016 Andy Scherzinger - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.preferences; @@ -55,6 +55,7 @@ public final class AppPreferencesImpl implements AppPreferences { */ public static final String AUTO_PREF__LAST_SEEN_VERSION_CODE = "lastSeenVersionCode"; public static final String STORAGE_PATH = "storage_path"; + public static final String DATA_STORAGE_LOCATION = "data_storage_location"; public static final String STORAGE_PATH_VALID = "storage_path_valid"; public static final String PREF__DARK_THEME = "dark_theme_mode"; public static final float DEFAULT_GRID_COLUMN = 3f; @@ -69,6 +70,8 @@ public final class AppPreferencesImpl implements AppPreferences { private static final String PREF__INSTANT_UPLOADING = "instant_uploading"; private static final String PREF__INSTANT_VIDEO_UPLOADING = "instant_video_uploading"; private static final String PREF__SHOW_HIDDEN_FILES = "show_hidden_files_pref"; + private static final String PREF__SORT_FOLDERS_BEFORE_FILES = "sort_folders_before_files"; + private static final String PREF__SORT_FAVORITES_FIRST = "sort_favorites_first"; private static final String PREF__SHOW_ECOSYSTEM_APPS = "show_ecosystem_apps"; private static final String PREF__LEGACY_CLEAN = "legacyClean"; private static final String PREF__KEYS_MIGRATION = "keysMigration"; @@ -87,7 +90,6 @@ public final class AppPreferencesImpl implements AppPreferences { private static final String PREF__SELECTED_ACCOUNT_NAME = "select_oc_account"; private static final String PREF__MIGRATED_USER_ID = "migrated_user_id"; private static final String PREF__PHOTO_SEARCH_TIMESTAMP = "photo_search_timestamp"; - private static final String PREF__POWER_CHECK_DISABLED = "power_check_disabled"; private static final String PREF__PIN_BRUTE_FORCE_COUNT = "pin_brute_force_count"; private static final String PREF__UID_PID = "uid_pid"; @@ -102,6 +104,15 @@ public final class AppPreferencesImpl implements AppPreferences { private static final String PREF__STORAGE_PERMISSION_REQUESTED = "storage_permission_requested"; private static final String PREF__IN_APP_REVIEW_DATA = "in_app_review_data"; + private static final String PREF__TWO_WAY_STATUS = "two_way_sync_status"; + private static final String PREF__TWO_WAY_SYNC_INTERVAL = "two_way_sync_interval"; + + private static final String PREF__STOP_DOWNLOAD_JOBS_ON_START = "stop_download_jobs_on_start"; + + private static final String PREF__PASSCODE_DELAY_IN_SECONDS = "passcode_delay_in_seconds"; + + private static final String PREF_LAST_DISPLAYED_ACCOUNT_NAME = "last_displayed_user"; + private static final String LOG_ENTRY = "log_entry"; private final Context context; @@ -219,6 +230,26 @@ public final class AppPreferencesImpl implements AppPreferences { preferences.edit().putBoolean(PREF__SHOW_HIDDEN_FILES, enabled).apply(); } + @Override + public boolean isSortFoldersBeforeFiles() { + return preferences.getBoolean(PREF__SORT_FOLDERS_BEFORE_FILES, true); + } + + @Override + public void setSortFoldersBeforeFiles(boolean enabled) { + preferences.edit().putBoolean(PREF__SORT_FOLDERS_BEFORE_FILES, enabled).apply(); + } + + @Override + public boolean isSortFavoritesFirst() { + return preferences.getBoolean(PREF__SORT_FAVORITES_FIRST, true); + } + + @Override + public void setSortFavoritesFirst(boolean enabled) { + preferences.edit().putBoolean(PREF__SORT_FAVORITES_FIRST, enabled).apply(); + } + @Override public boolean isShowEcosystemApps() { return preferences.getBoolean(PREF__SHOW_ECOSYSTEM_APPS, true); @@ -300,11 +331,6 @@ public final class AppPreferencesImpl implements AppPreferences { }; } - @Override - public boolean isFingerprintUnlockEnabled() { - return preferences.getBoolean(SettingsActivity.PREFERENCE_USE_FINGERPRINT, false); - } - @Override public String getFolderLayout(OCFile folder) { return getFolderPreference(context, @@ -329,7 +355,7 @@ public final class AppPreferencesImpl implements AppPreferences { userAccountManager.getUser(), PREF__FOLDER_SORT_ORDER, folder, - FileSortOrder.sort_a_to_z.name)); + FileSortOrder.SORT_A_TO_Z.name)); } @Override @@ -343,7 +369,7 @@ public final class AppPreferencesImpl implements AppPreferences { @Override public FileSortOrder getSortOrderByType(FileSortOrder.Type type) { - return getSortOrderByType(type, FileSortOrder.sort_a_to_z); + return getSortOrderByType(type, FileSortOrder.SORT_A_TO_Z); } @Override @@ -684,16 +710,6 @@ public final class AppPreferencesImpl implements AppPreferences { return preferenceName + "_" + folderIdString; } - @Override - public boolean isPowerCheckDisabled() { - return preferences.getBoolean(PREF__POWER_CHECK_DISABLED, false); - } - - @Override - public void setPowerCheckDisabled(boolean value) { - preferences.edit().putBoolean(PREF__POWER_CHECK_DISABLED, value).apply(); - } - public void increasePinWrongAttempts() { int count = preferences.getInt(PREF__PIN_BRUTE_FORCE_COUNT, 0); preferences.edit().putInt(PREF__PIN_BRUTE_FORCE_COUNT, count + 1).apply(); @@ -789,4 +805,54 @@ public final class AppPreferencesImpl implements AppPreferences { public String getLastSelectedMediaFolder() { return preferences.getString(PREF__MEDIA_FOLDER_LAST_PATH, OCFile.ROOT_PATH); } + + @Override + public void setTwoWaySyncStatus(boolean value) { + preferences.edit().putBoolean(PREF__TWO_WAY_STATUS, value).apply(); + } + + @Override + public boolean isTwoWaySyncEnabled() { + return preferences.getBoolean(PREF__TWO_WAY_STATUS, true); + } + + @Override + public void setTwoWaySyncInterval(Long value) { + preferences.edit().putLong(PREF__TWO_WAY_SYNC_INTERVAL, value).apply(); + } + + @Override + public Long getTwoWaySyncInterval() { + return preferences.getLong(PREF__TWO_WAY_SYNC_INTERVAL, 15L); + } + + @Override + public boolean shouldStopDownloadJobsOnStart() { + return preferences.getBoolean(PREF__STOP_DOWNLOAD_JOBS_ON_START, true); + } + + @Override + public void setStopDownloadJobsOnStart(boolean value) { + preferences.edit().putBoolean(PREF__STOP_DOWNLOAD_JOBS_ON_START, value).apply(); + } + + @Override + public int getPassCodeDelay() { + return preferences.getInt(PREF__PASSCODE_DELAY_IN_SECONDS, 0); + } + + @Override + public void setPassCodeDelay(int value) { + preferences.edit().putInt(PREF__PASSCODE_DELAY_IN_SECONDS, value).apply(); + } + + @Override + public String getLastDisplayedAccountName() { + return preferences.getString(PREF_LAST_DISPLAYED_ACCOUNT_NAME, null); + } + + @Override + public void setLastDisplayedAccountName(String lastDisplayedAccountName) { + preferences.edit().putString(PREF_LAST_DISPLAYED_ACCOUNT_NAME, lastDisplayedAccountName).apply(); + } } diff --git a/app/src/main/java/com/nextcloud/client/preferences/DarkMode.java b/app/src/main/java/com/nextcloud/client/preferences/DarkMode.java index 1bf454c..c516893 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/DarkMode.java +++ b/app/src/main/java/com/nextcloud/client/preferences/DarkMode.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2019 Tobias Kaminsky * SPDX-FileCopyrightText: 2019 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.preferences; diff --git a/app/src/main/java/com/nextcloud/client/preferences/PreferencesModule.java b/app/src/main/java/com/nextcloud/client/preferences/PreferencesModule.java index d836dbb..09f4a8a 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/PreferencesModule.java +++ b/app/src/main/java/com/nextcloud/client/preferences/PreferencesModule.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.preferences; diff --git a/app/src/main/java/com/nextcloud/client/preferences/SubFolderRule.kt b/app/src/main/java/com/nextcloud/client/preferences/SubFolderRule.kt index b617470..8541203 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/SubFolderRule.kt +++ b/app/src/main/java/com/nextcloud/client/preferences/SubFolderRule.kt @@ -5,11 +5,13 @@ * Copyright (C) 2023 Dean Birch * Copyright (C) 2023 Nextcloud GmbH * - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.preferences enum class SubFolderRule { - YEAR_MONTH, YEAR, YEAR_MONTH_DAY + YEAR_MONTH, + YEAR, + YEAR_MONTH_DAY } diff --git a/app/src/main/java/com/nextcloud/client/utils/IntentUtil.kt b/app/src/main/java/com/nextcloud/client/utils/IntentUtil.kt index 19e2d20..1f3dfa2 100644 --- a/app/src/main/java/com/nextcloud/client/utils/IntentUtil.kt +++ b/app/src/main/java/com/nextcloud/client/utils/IntentUtil.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.utils @@ -15,25 +15,22 @@ import com.owncloud.android.datamodel.OCFile object IntentUtil { @JvmStatic - public fun createSendIntent(context: Context, file: OCFile): Intent = - createBaseSendFileIntent().apply { - action = Intent.ACTION_SEND - type = file.mimeType - putExtra(Intent.EXTRA_STREAM, file.getExposedFileUri(context)) - } + public fun createSendIntent(context: Context, file: OCFile): Intent = createBaseSendFileIntent().apply { + action = Intent.ACTION_SEND + type = file.mimeType + putExtra(Intent.EXTRA_STREAM, file.getExposedFileUri(context)) + } @JvmStatic - public fun createSendIntent(context: Context, files: Array): Intent = - createBaseSendFileIntent().apply { - action = Intent.ACTION_SEND_MULTIPLE - type = getUniqueMimetype(files) - putParcelableArrayListExtra(Intent.EXTRA_STREAM, getExposedFileUris(context, files)) - } + public fun createSendIntent(context: Context, files: Array): Intent = createBaseSendFileIntent().apply { + action = Intent.ACTION_SEND_MULTIPLE + type = getUniqueMimetype(files) + putParcelableArrayListExtra(Intent.EXTRA_STREAM, getExposedFileUris(context, files)) + } - private fun createBaseSendFileIntent(): Intent = - Intent().apply { - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - } + private fun createBaseSendFileIntent(): Intent = Intent().apply { + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } private fun getUniqueMimetype(files: Array): String? = when { files.distinctBy { it.mimeType }.size > 1 -> "*/*" diff --git a/app/src/main/java/com/nextcloud/client/utils/Throttler.kt b/app/src/main/java/com/nextcloud/client/utils/Throttler.kt index 4669c2d..3e54772 100644 --- a/app/src/main/java/com/nextcloud/client/utils/Throttler.kt +++ b/app/src/main/java/com/nextcloud/client/utils/Throttler.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2021 Álvaro Brey * SPDX-FileCopyrightText: 2021 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.utils diff --git a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationActivity.kt b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationActivity.kt index 13defe1..57b5e2a 100644 --- a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationActivity.kt +++ b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationActivity.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.widget diff --git a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationInterface.kt b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationInterface.kt index c436f6d..f62adca 100644 --- a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationInterface.kt +++ b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetConfigurationInterface.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.widget diff --git a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetProvider.kt b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetProvider.kt index 61e807d..18fc207 100644 --- a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetProvider.kt +++ b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetProvider.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.widget diff --git a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt index a074c72..a55157d 100644 --- a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt +++ b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetService.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.widget @@ -11,33 +11,25 @@ import android.appwidget.AppWidgetManager import android.content.Context import android.content.Intent import android.graphics.Bitmap -import android.net.Uri import android.view.View import android.widget.RemoteViews import android.widget.RemoteViewsService -import com.bumptech.glide.Glide -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.load.model.StreamEncoder -import com.bumptech.glide.load.resource.file.FileToStreamDecoder -import com.bumptech.glide.request.FutureTarget +import androidx.core.graphics.drawable.toBitmap +import androidx.core.net.toUri import com.nextcloud.android.lib.resources.dashboard.DashboardGetWidgetItemsRemoteOperation import com.nextcloud.android.lib.resources.dashboard.DashboardWidgetItem import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.network.ClientFactory +import com.nextcloud.utils.GlideHelper import com.owncloud.android.R +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.utils.BitmapUtils -import com.owncloud.android.utils.DisplayUtils.SVG_SIZE -import com.owncloud.android.utils.glide.CustomGlideStreamLoader -import com.owncloud.android.utils.glide.CustomGlideUriLoader -import com.owncloud.android.utils.svg.SVGorImage -import com.owncloud.android.utils.svg.SvgOrImageBitmapTranscoder -import com.owncloud.android.utils.svg.SvgOrImageDecoder import dagger.android.AndroidInjection import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import java.io.InputStream +import kotlinx.coroutines.withContext import javax.inject.Inject class DashboardWidgetService : RemoteViewsService() { @@ -55,15 +47,13 @@ class DashboardWidgetService : RemoteViewsService() { AndroidInjection.inject(this) } - override fun onGetViewFactory(intent: Intent): RemoteViewsFactory { - return StackRemoteViewsFactory( - this.applicationContext, - userAccountManager, - clientFactory, - intent, - widgetRepository - ) - } + override fun onGetViewFactory(intent: Intent): RemoteViewsFactory = StackRemoteViewsFactory( + this.applicationContext, + userAccountManager, + clientFactory, + intent, + widgetRepository + ) } class StackRemoteViewsFactory( @@ -123,29 +113,24 @@ class StackRemoteViewsFactory( widgetItems = emptyList() } - override fun getCount(): Int { - return if (hasLoadMore && widgetItems.isNotEmpty()) { - widgetItems.size + 1 - } else { - widgetItems.size - } + override fun getCount(): Int = if (hasLoadMore && widgetItems.isNotEmpty()) { + widgetItems.size + 1 + } else { + widgetItems.size } - override fun getViewAt(position: Int): RemoteViews { - return if (position == widgetItems.size) { - createLoadMoreView() - } else { - createItemView(position) - } + override fun getViewAt(position: Int): RemoteViews = if (position == widgetItems.size) { + createLoadMoreView() + } else { + createItemView(position) } - private fun createLoadMoreView(): RemoteViews { - return RemoteViews(context.packageName, R.layout.widget_item_load_more).apply { - val clickIntent = Intent(Intent.ACTION_VIEW, Uri.parse(widgetConfiguration.moreButton?.link)) + private fun createLoadMoreView(): RemoteViews = + RemoteViews(context.packageName, R.layout.widget_item_load_more).apply { + val clickIntent = Intent(Intent.ACTION_VIEW, widgetConfiguration.moreButton?.link?.toUri()) setTextViewText(R.id.load_more, widgetConfiguration.moreButton?.text) setOnClickFillInIntent(R.id.load_more_container, clickIntent) } - } // we will switch soon to coil and then streamline all of this // Kotlin cannot catch multiple exception types at same time @@ -165,60 +150,42 @@ class StackRemoteViewsFactory( updateTexts(widgetItem, this) if (widgetItem.link.isNotEmpty()) { - val clickIntent = Intent(Intent.ACTION_VIEW, Uri.parse(widgetItem.link)) + val clickIntent = Intent(Intent.ACTION_VIEW, widgetItem.link.toUri()) setOnClickFillInIntent(R.id.text_container, clickIntent) } } } - @Suppress("TooGenericExceptionCaught") private fun loadIcon(widgetItem: DashboardWidgetItem, remoteViews: RemoteViews) { - val isIconSVG = Uri.parse(widgetItem.iconUrl).encodedPath!!.endsWith(".svg") - val source: FutureTarget = if (isIconSVG) { - loadSVGIcon(widgetItem) - } else { - loadBitmapIcon(widgetItem) - } + CoroutineScope(Dispatchers.IO).launch { + val client = OwnCloudClientManagerFactory.getDefaultSingleton() + .getNextcloudClientFor(userAccountManager.user.toOwnCloudAccount(), context) + val pictureDrawable = GlideHelper.getDrawable(context, client, widgetItem.iconUrl) + val bitmap = pictureDrawable?.toBitmap() ?: return@launch + withContext(Dispatchers.Main) { + remoteViews.setRemoteImageView(bitmap) + return@withContext + } + } + } + + @Suppress("TooGenericExceptionCaught") + private fun RemoteViews.setRemoteImageView(source: Bitmap) { try { val bitmap: Bitmap = if (widgetConfiguration.roundIcon) { - BitmapUtils.roundBitmap(source.get()) + BitmapUtils.roundBitmap(source) } else { - source.get() + source } - remoteViews.setImageViewBitmap(R.id.icon, bitmap) + setImageViewBitmap(R.id.icon, bitmap) } catch (e: Exception) { Log_OC.d(TAG, "Error setting icon", e) - remoteViews.setImageViewResource(R.id.icon, R.drawable.ic_dashboard) + setImageViewResource(R.id.icon, R.drawable.ic_dashboard) } } - private fun loadSVGIcon(widgetItem: DashboardWidgetItem): FutureTarget { - return Glide.with(context) - .using( - CustomGlideUriLoader(userAccountManager.user, clientFactory), - InputStream::class.java - ) - .from(Uri::class.java) - .`as`(SVGorImage::class.java) - .transcode(SvgOrImageBitmapTranscoder(SVG_SIZE, SVG_SIZE), Bitmap::class.java) - .sourceEncoder(StreamEncoder()) - .cacheDecoder(FileToStreamDecoder(SvgOrImageDecoder())) - .decoder(SvgOrImageDecoder()) - .diskCacheStrategy(DiskCacheStrategy.SOURCE) - .load(Uri.parse(widgetItem.iconUrl)) - .into(SVG_SIZE, SVG_SIZE) - } - - private fun loadBitmapIcon(widgetItem: DashboardWidgetItem): FutureTarget { - return Glide.with(context) - .using(CustomGlideStreamLoader(widgetConfiguration.user.get(), clientFactory)) - .load(widgetItem.iconUrl) - .asBitmap() - .into(SVG_SIZE, SVG_SIZE) - } - private fun updateTexts(widgetItem: DashboardWidgetItem, remoteViews: RemoteViews) { remoteViews.setTextViewText(R.id.title, widgetItem.title) @@ -230,25 +197,17 @@ class StackRemoteViewsFactory( } } - override fun getLoadingView(): RemoteViews? { - return null + override fun getLoadingView(): RemoteViews? = null + + override fun getViewTypeCount(): Int = if (hasLoadMore) { + 2 + } else { + 1 } - override fun getViewTypeCount(): Int { - return if (hasLoadMore) { - 2 - } else { - 1 - } - } + override fun getItemId(position: Int): Long = position.toLong() - override fun getItemId(position: Int): Long { - return position.toLong() - } - - override fun hasStableIds(): Boolean { - return true - } + override fun hasStableIds(): Boolean = true companion object { private val TAG = DashboardWidgetService::class.simpleName diff --git a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetUpdater.kt b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetUpdater.kt index 9806202..3430664 100644 --- a/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetUpdater.kt +++ b/app/src/main/java/com/nextcloud/client/widget/DashboardWidgetUpdater.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.widget @@ -11,28 +11,23 @@ import android.app.PendingIntent import android.appwidget.AppWidgetManager import android.content.Context import android.content.Intent -import android.graphics.Bitmap -import android.net.Uri import android.os.Build import android.view.View import android.widget.RemoteViews -import com.bumptech.glide.Glide -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.load.model.StreamEncoder -import com.bumptech.glide.load.resource.file.FileToStreamDecoder -import com.bumptech.glide.request.animation.GlideAnimation +import androidx.core.graphics.drawable.toBitmap +import androidx.core.net.toUri import com.bumptech.glide.request.target.AppWidgetTarget import com.nextcloud.android.lib.resources.dashboard.DashboardButton import com.nextcloud.client.account.CurrentAccountProvider import com.nextcloud.client.network.ClientFactory +import com.nextcloud.utils.GlideHelper import com.owncloud.android.R +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.utils.BitmapUtils -import com.owncloud.android.utils.DisplayUtils.SVG_SIZE -import com.owncloud.android.utils.glide.CustomGlideUriLoader -import com.owncloud.android.utils.svg.SVGorImage -import com.owncloud.android.utils.svg.SvgOrImageBitmapTranscoder -import com.owncloud.android.utils.svg.SvgOrImageDecoder -import java.io.InputStream +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject class DashboardWidgetUpdater @Inject constructor( @@ -50,7 +45,7 @@ class DashboardWidgetUpdater @Inject constructor( ) { val intent = Intent(context, DashboardWidgetService::class.java).apply { putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) - data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) + data = toUri(Intent.URI_INTENT_SCHEME).toUri() } val views = RemoteViews(context.packageName, R.layout.dashboard_widget).apply { @@ -61,7 +56,10 @@ class DashboardWidgetUpdater @Inject constructor( setAddButton(addButton, appWidgetId, this) setPendingReload(this, appWidgetId) setPendingClick(this) - loadIcon(appWidgetId, iconUrl, this) + + if (iconUrl.isNotEmpty()) { + loadIcon(appWidgetId, iconUrl, this) + } } appWidgetManager.run { @@ -130,7 +128,7 @@ class DashboardWidgetUpdater @Inject constructor( val intent = Intent(context, DashboardWidgetProvider::class.java).apply { setPackage(context.packageName) action = DashboardWidgetProvider.OPEN_INTENT - data = Uri.parse(addButton.link) + data = addButton.link.toUri() } return PendingIntent.getBroadcast( @@ -156,28 +154,17 @@ class DashboardWidgetUpdater @Inject constructor( // endregion private fun loadIcon(appWidgetId: Int, iconUrl: String, remoteViews: RemoteViews) { - val iconTarget = object : AppWidgetTarget(context, remoteViews, R.id.icon, appWidgetId) { - override fun onResourceReady(resource: Bitmap?, glideAnimation: GlideAnimation?) { - if (resource != null) { - val tintedBitmap = BitmapUtils.tintImage(resource, R.color.black) - super.onResourceReady(tintedBitmap, glideAnimation) - } + val target = AppWidgetTarget(context, R.id.icon, remoteViews, appWidgetId) + CoroutineScope(Dispatchers.IO).launch { + val client = OwnCloudClientManagerFactory.getDefaultSingleton() + .getNextcloudClientFor(accountProvider.user.toOwnCloudAccount(), context) + val drawable = GlideHelper.getDrawable(context, client, iconUrl) + val bitmap = drawable?.toBitmap() ?: return@launch + val tintedBitmap = BitmapUtils.tintImage(bitmap, R.color.black) + + withContext(Dispatchers.Main) { + target.onResourceReady(tintedBitmap, null) } } - - Glide.with(context) - .using( - CustomGlideUriLoader(accountProvider.user, clientFactory), - InputStream::class.java - ) - .from(Uri::class.java) - .`as`(SVGorImage::class.java) - .transcode(SvgOrImageBitmapTranscoder(SVG_SIZE, SVG_SIZE), Bitmap::class.java) - .sourceEncoder(StreamEncoder()) - .cacheDecoder(FileToStreamDecoder(SvgOrImageDecoder())) - .decoder(SvgOrImageDecoder()) - .diskCacheStrategy(DiskCacheStrategy.SOURCE) - .load(Uri.parse(iconUrl)) - .into(iconTarget) } } diff --git a/app/src/main/java/com/nextcloud/client/widget/WidgetConfiguration.kt b/app/src/main/java/com/nextcloud/client/widget/WidgetConfiguration.kt index 49503fe..89b5ea3 100644 --- a/app/src/main/java/com/nextcloud/client/widget/WidgetConfiguration.kt +++ b/app/src/main/java/com/nextcloud/client/widget/WidgetConfiguration.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.widget diff --git a/app/src/main/java/com/nextcloud/client/widget/WidgetRepository.kt b/app/src/main/java/com/nextcloud/client/widget/WidgetRepository.kt index da12dd4..c72f1fb 100644 --- a/app/src/main/java/com/nextcloud/client/widget/WidgetRepository.kt +++ b/app/src/main/java/com/nextcloud/client/widget/WidgetRepository.kt @@ -3,11 +3,12 @@ * * SPDX-FileCopyrightText: 2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.client.widget import android.content.SharedPreferences +import androidx.core.content.edit import com.nextcloud.android.lib.resources.dashboard.DashBoardButtonType import com.nextcloud.android.lib.resources.dashboard.DashboardButton import com.nextcloud.android.lib.resources.dashboard.DashboardWidget @@ -21,48 +22,48 @@ class WidgetRepository @Inject constructor( val preferences: SharedPreferences ) { fun saveWidget(widgetId: Int, widget: DashboardWidget, user: User) { - val editor: SharedPreferences.Editor = preferences - .edit() - .putString(PREF__WIDGET_ID + widgetId, widget.id) - .putString(PREF__WIDGET_TITLE + widgetId, widget.title) - .putString(PREF__WIDGET_ICON + widgetId, widget.iconUrl) - .putBoolean(PREF__WIDGET_ROUND_ICON + widgetId, widget.roundIcons) - .putString(PREF__WIDGET_USER + widgetId, user.accountName) - val buttonList = widget.buttons - if (buttonList != null && buttonList.isNotEmpty()) { - for (button in buttonList) { - if (button.type == DashBoardButtonType.NEW) { - editor - .putString(PREF__WIDGET_ADD_BUTTON_TYPE + widgetId, button.type.toString()) - .putString(PREF__WIDGET_ADD_BUTTON_URL + widgetId, button.link) - .putString(PREF__WIDGET_ADD_BUTTON_TEXT + widgetId, button.text) - } - if (button.type == DashBoardButtonType.MORE) { - editor - .putString(PREF__WIDGET_MORE_BUTTON_TYPE + widgetId, button.type.toString()) - .putString(PREF__WIDGET_MORE_BUTTON_URL + widgetId, button.link) - .putString(PREF__WIDGET_MORE_BUTTON_TEXT + widgetId, button.text) + preferences + .edit { + putString(PREF__WIDGET_ID + widgetId, widget.id) + .putString(PREF__WIDGET_TITLE + widgetId, widget.title) + .putString(PREF__WIDGET_ICON + widgetId, widget.iconUrl) + .putBoolean(PREF__WIDGET_ROUND_ICON + widgetId, widget.roundIcons) + .putString(PREF__WIDGET_USER + widgetId, user.accountName) + val buttonList = widget.buttons + if (!buttonList.isNullOrEmpty()) { + for (button in buttonList) { + if (button.type == DashBoardButtonType.NEW) { + this + .putString(PREF__WIDGET_ADD_BUTTON_TYPE + widgetId, button.type.toString()) + .putString(PREF__WIDGET_ADD_BUTTON_URL + widgetId, button.link) + .putString(PREF__WIDGET_ADD_BUTTON_TEXT + widgetId, button.text) + } + if (button.type == DashBoardButtonType.MORE) { + this + .putString(PREF__WIDGET_MORE_BUTTON_TYPE + widgetId, button.type.toString()) + .putString(PREF__WIDGET_MORE_BUTTON_URL + widgetId, button.link) + .putString(PREF__WIDGET_MORE_BUTTON_TEXT + widgetId, button.text) + } + } } } - } - editor.apply() } fun deleteWidget(widgetId: Int) { preferences - .edit() - .remove(PREF__WIDGET_ID + widgetId) - .remove(PREF__WIDGET_TITLE + widgetId) - .remove(PREF__WIDGET_ICON + widgetId) - .remove(PREF__WIDGET_ROUND_ICON + widgetId) - .remove(PREF__WIDGET_USER + widgetId) - .remove(PREF__WIDGET_ADD_BUTTON_TEXT + widgetId) - .remove(PREF__WIDGET_ADD_BUTTON_URL + widgetId) - .remove(PREF__WIDGET_ADD_BUTTON_TYPE + widgetId) - .remove(PREF__WIDGET_MORE_BUTTON_TEXT + widgetId) - .remove(PREF__WIDGET_MORE_BUTTON_URL + widgetId) - .remove(PREF__WIDGET_MORE_BUTTON_TYPE + widgetId) - .apply() + .edit { + remove(PREF__WIDGET_ID + widgetId) + .remove(PREF__WIDGET_TITLE + widgetId) + .remove(PREF__WIDGET_ICON + widgetId) + .remove(PREF__WIDGET_ROUND_ICON + widgetId) + .remove(PREF__WIDGET_USER + widgetId) + .remove(PREF__WIDGET_ADD_BUTTON_TEXT + widgetId) + .remove(PREF__WIDGET_ADD_BUTTON_URL + widgetId) + .remove(PREF__WIDGET_ADD_BUTTON_TYPE + widgetId) + .remove(PREF__WIDGET_MORE_BUTTON_TEXT + widgetId) + .remove(PREF__WIDGET_MORE_BUTTON_URL + widgetId) + .remove(PREF__WIDGET_MORE_BUTTON_TYPE + widgetId) + } } fun getWidget(widgetId: Int): WidgetConfiguration { diff --git a/app/src/main/java/com/nextcloud/model/HTTPStatusCodes.kt b/app/src/main/java/com/nextcloud/model/HTTPStatusCodes.kt index 1bdd7b4..05a44d6 100644 --- a/app/src/main/java/com/nextcloud/model/HTTPStatusCodes.kt +++ b/app/src/main/java/com/nextcloud/model/HTTPStatusCodes.kt @@ -1,13 +1,14 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.model @Suppress("MagicNumber") enum class HTTPStatusCodes(val code: Int) { + SUCCESS(200), NOT_FOUND(404) } diff --git a/app/src/main/java/com/nextcloud/model/OCFileFilterType.kt b/app/src/main/java/com/nextcloud/model/OCFileFilterType.kt new file mode 100644 index 0000000..81451c2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/model/OCFileFilterType.kt @@ -0,0 +1,13 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.model + +enum class OCFileFilterType { + Shared, + Favorite +} diff --git a/app/src/main/java/com/nextcloud/model/OfflineOperationType.kt b/app/src/main/java/com/nextcloud/model/OfflineOperationType.kt new file mode 100644 index 0000000..7f31258 --- /dev/null +++ b/app/src/main/java/com/nextcloud/model/OfflineOperationType.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.model + +sealed class OfflineOperationType { + abstract val type: String + + data class CreateFolder(override val type: String, var path: String) : OfflineOperationType() + + data class CreateFile( + override val type: String, + val localPath: String, + var remotePath: String, + var mimeType: String + ) : OfflineOperationType() + + data class RenameFile(override val type: String, var ocFileId: Long, val newName: String) : OfflineOperationType() + + data class RemoveFile(override val type: String, var path: String) : OfflineOperationType() +} + +enum class OfflineOperationRawType { + CreateFolder, + CreateFile, + RenameFile, + RemoveFile +} diff --git a/app/src/main/java/com/nextcloud/model/SearchResultEntryType.kt b/app/src/main/java/com/nextcloud/model/SearchResultEntryType.kt new file mode 100644 index 0000000..b71e4ca --- /dev/null +++ b/app/src/main/java/com/nextcloud/model/SearchResultEntryType.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.model + +import com.owncloud.android.R + +enum class SearchResultEntryType { + CalendarEvent, + Folder, + Note, + Contact, + Deck, + Unknown; + + fun iconId(): Int = when (this) { + CalendarEvent -> R.drawable.file_calendar + Folder -> R.drawable.folder + Note -> R.drawable.ic_edit + Contact -> R.drawable.file_vcard + Deck -> R.drawable.ic_deck + Unknown -> R.drawable.ic_find_in_page + } +} diff --git a/app/src/main/java/com/nextcloud/model/ShareeEntry.kt b/app/src/main/java/com/nextcloud/model/ShareeEntry.kt new file mode 100644 index 0000000..05e9a6a --- /dev/null +++ b/app/src/main/java/com/nextcloud/model/ShareeEntry.kt @@ -0,0 +1,71 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.model + +import android.content.ContentValues +import com.owncloud.android.db.ProviderMeta.ProviderTableMeta +import com.owncloud.android.lib.resources.files.model.RemoteFile +import com.owncloud.android.lib.resources.shares.ShareType + +data class ShareeEntry( + val filePath: String?, + val accountOwner: String, + val fileOwnerId: String?, + val shareWithDisplayName: String?, + val shareWithUserId: String?, + val shareType: Int +) { + companion object { + /** + * Extracts a list of share-related ContentValues from a given RemoteFile. + * + * Each RemoteFile can be shared with multiple users (sharees), and this function converts each + * sharee into a ContentValues object, representing a row for insertion into a database. + * + * @param remoteFile The RemoteFile object containing sharee information. + * @param accountName The name of the user account that owns this RemoteFile. + * @return A list of ContentValues representing each share entry, or null if no sharees are found. + */ + fun getContentValues(remoteFile: RemoteFile, accountName: String): List? { + if (remoteFile.sharees.isNullOrEmpty()) { + return null + } + + val result = arrayListOf() + + for (share in remoteFile.sharees) { + val shareType: ShareType? = share?.shareType + if (shareType == null) { + continue + } + + val contentValue = ShareeEntry( + remoteFile.remotePath, + accountName, + remoteFile.ownerId, + share.displayName, + share.userId, + shareType.value + ).toContentValues() + + result.add(contentValue) + } + + return result + } + } + + private fun toContentValues(): ContentValues = ContentValues().apply { + put(ProviderTableMeta.OCSHARES_PATH, filePath) + put(ProviderTableMeta.OCSHARES_ACCOUNT_OWNER, accountOwner) + put(ProviderTableMeta.OCSHARES_USER_ID, fileOwnerId) + put(ProviderTableMeta.OCSHARES_SHARE_WITH_DISPLAY_NAME, shareWithDisplayName) + put(ProviderTableMeta.OCSHARES_SHARE_WITH, shareWithUserId) + put(ProviderTableMeta.OCSHARES_SHARE_TYPE, shareType) + } +} diff --git a/app/src/main/java/com/nextcloud/model/ToolbarItem.kt b/app/src/main/java/com/nextcloud/model/ToolbarItem.kt new file mode 100644 index 0000000..8995acc --- /dev/null +++ b/app/src/main/java/com/nextcloud/model/ToolbarItem.kt @@ -0,0 +1,38 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.model + +import android.view.Menu +import com.owncloud.android.R + +enum class ToolbarItem(val navId: Int, val titleId: Int, val style: ToolbarStyle) { + NONE(Menu.NONE, R.string.drawer_item_all_files, ToolbarStyle.SEARCH), + ALL_FILES(R.id.nav_all_files, R.string.drawer_item_all_files, ToolbarStyle.SEARCH), + PERSONAL_FILES(R.id.nav_personal_files, R.string.drawer_item_personal_files, ToolbarStyle.SEARCH), + ACTIVITIES(R.id.nav_activity, R.string.drawer_item_activities, ToolbarStyle.PLAIN), + FAVORITES(R.id.nav_favorites, R.string.drawer_item_favorites, ToolbarStyle.PLAIN), + GALLERY(R.id.nav_gallery, R.string.drawer_item_gallery, ToolbarStyle.PLAIN), + SHARED(R.id.nav_shared, R.string.drawer_item_shared, ToolbarStyle.PLAIN), + GROUP_FOLDERS(R.id.nav_groupfolders, R.string.drawer_item_groupfolders, ToolbarStyle.PLAIN), + ON_DEVICE(R.id.nav_on_device, R.string.drawer_item_on_device, ToolbarStyle.PLAIN), + RECENTLY_MODIFIED(R.id.nav_recently_modified, R.string.drawer_item_recently_modified, ToolbarStyle.PLAIN), + ASSISTANT(R.id.nav_assistant, R.string.drawer_item_assistant, ToolbarStyle.PLAIN), + UPLOADS(R.id.nav_uploads, R.string.drawer_item_uploads_list, ToolbarStyle.PLAIN), + SETTINGS(R.id.nav_settings, R.string.actionbar_settings, ToolbarStyle.PLAIN), + COMMUNITY(R.id.nav_community, R.string.drawer_community, ToolbarStyle.PLAIN), + TRASHBIN(R.id.nav_trashbin, R.string.drawer_item_trashbin, ToolbarStyle.PLAIN); + + companion object { + fun fromNavId(navId: Int): ToolbarItem? = entries.find { it.navId == navId } + } +} + +enum class ToolbarStyle { + PLAIN, + SEARCH +} diff --git a/app/src/main/java/com/nextcloud/model/WorkerState.kt b/app/src/main/java/com/nextcloud/model/WorkerState.kt index d328a96..6a1cca1 100644 --- a/app/src/main/java/com/nextcloud/model/WorkerState.kt +++ b/app/src/main/java/com/nextcloud/model/WorkerState.kt @@ -1,18 +1,20 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.model import com.nextcloud.client.account.User -import com.owncloud.android.db.OCUpload +import com.owncloud.android.datamodel.OCFile import com.owncloud.android.operations.DownloadFileOperation sealed class WorkerState { - object Idle : WorkerState() - class Download(var user: User?, var currentDownload: DownloadFileOperation?) : WorkerState() - class Upload(var user: User?, var uploads: List) : WorkerState() + data class DownloadFinished(var currentFile: OCFile?) : WorkerState() + data class DownloadStarted(var user: User?, var currentDownload: DownloadFileOperation?) : WorkerState() + data class UploadFinished(var currentFile: OCFile?) : WorkerState() + data class UploadStarted(var user: User?) : WorkerState() + data object OfflineOperationsCompleted : WorkerState() } diff --git a/app/src/main/java/com/nextcloud/model/WorkerStateLiveData.kt b/app/src/main/java/com/nextcloud/model/WorkerStateLiveData.kt index d5dfe91..28d92b8 100644 --- a/app/src/main/java/com/nextcloud/model/WorkerStateLiveData.kt +++ b/app/src/main/java/com/nextcloud/model/WorkerStateLiveData.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.model @@ -18,10 +18,8 @@ class WorkerStateLiveData private constructor() : LiveData() { companion object { private var instance: WorkerStateLiveData? = null - fun instance(): WorkerStateLiveData { - return instance ?: synchronized(this) { - instance ?: WorkerStateLiveData().also { instance = it } - } + fun instance(): WorkerStateLiveData = instance ?: synchronized(this) { + instance ?: WorkerStateLiveData().also { instance = it } } } } diff --git a/app/src/main/java/com/nextcloud/receiver/NetworkChangeReceiver.kt b/app/src/main/java/com/nextcloud/receiver/NetworkChangeReceiver.kt new file mode 100644 index 0000000..d9d7cbe --- /dev/null +++ b/app/src/main/java/com/nextcloud/receiver/NetworkChangeReceiver.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.nextcloud.client.network.ConnectivityService + +interface NetworkChangeListener { + fun networkAndServerConnectionListener(isNetworkAndServerAvailable: Boolean) +} + +class NetworkChangeReceiver( + private val listener: NetworkChangeListener, + private val connectivityService: ConnectivityService +) : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent?) { + connectivityService.isNetworkAndServerAvailable { + listener.networkAndServerConnectionListener(it) + } + } +} diff --git a/app/src/main/java/com/nextcloud/repository/ClientRepository.kt b/app/src/main/java/com/nextcloud/repository/ClientRepository.kt new file mode 100644 index 0000000..6e42332 --- /dev/null +++ b/app/src/main/java/com/nextcloud/repository/ClientRepository.kt @@ -0,0 +1,45 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.repository + +import com.nextcloud.common.NextcloudClient +import com.owncloud.android.lib.common.OwnCloudClient + +/** + * Interface defining methods to retrieve Nextcloud and OwnCloudClient clients. + * Provides both callback-based and suspend function versions for flexibility in usage. + */ +interface ClientRepository { + /** + * Retrieves an instance of [NextcloudClient] using a callback. + * + * @param onComplete A callback function that receives the [NextcloudClient] instance once available. + */ + fun getNextcloudClient(onComplete: (NextcloudClient) -> Unit) + + /** + * Retrieves an instance of [NextcloudClient] as a suspend function. + * + * @return The [NextcloudClient] instance, or `null` if it cannot be retrieved. + */ + suspend fun getNextcloudClient(): NextcloudClient? + + /** + * Retrieves an instance of [OwnCloudClient] using a callback. + * + * @param onComplete A callback function that receives the [OwnCloudClient] instance once available. + */ + fun getOwncloudClient(onComplete: (OwnCloudClient) -> Unit) + + /** + * Retrieves an instance of [OwnCloudClient] as a suspend function. + * + * @return The [OwnCloudClient] instance, or `null` if it cannot be retrieved. + */ + suspend fun getOwncloudClient(): OwnCloudClient? +} diff --git a/app/src/main/java/com/nextcloud/repository/RemoteClientRepository.kt b/app/src/main/java/com/nextcloud/repository/RemoteClientRepository.kt new file mode 100644 index 0000000..5bdfa82 --- /dev/null +++ b/app/src/main/java/com/nextcloud/repository/RemoteClientRepository.kt @@ -0,0 +1,68 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.repository + +import android.content.Context +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.nextcloud.client.account.User +import com.nextcloud.common.NextcloudClient +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory +import com.owncloud.android.lib.common.utils.Log_OC +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@Suppress("TooGenericExceptionCaught", "DEPRECATION") +class RemoteClientRepository(private val user: User, private val context: Context, lifecycleOwner: LifecycleOwner) : + ClientRepository { + private val tag = "ClientRepository" + private val clientFactory = OwnCloudClientManagerFactory.getDefaultSingleton() + private val scope = lifecycleOwner.lifecycleScope + + override fun getNextcloudClient(onComplete: (NextcloudClient) -> Unit) { + scope.launch(Dispatchers.IO) { + try { + val client = clientFactory.getNextcloudClientFor(user.toOwnCloudAccount(), context) + onComplete(client) + } catch (e: Exception) { + Log_OC.d(tag, "Exception caught getNextcloudClient(): $e") + } + } + } + + override suspend fun getNextcloudClient(): NextcloudClient? = withContext(Dispatchers.IO) { + try { + clientFactory.getNextcloudClientFor(user.toOwnCloudAccount(), context) + } catch (e: Exception) { + Log_OC.d(tag, "Exception caught getNextcloudClient(): $e") + null + } + } + + override fun getOwncloudClient(onComplete: (OwnCloudClient) -> Unit) { + scope.launch(Dispatchers.IO) { + try { + val client = clientFactory.getClientFor(user.toOwnCloudAccount(), context) + onComplete(client) + } catch (e: Exception) { + Log_OC.d(tag, "Exception caught getOwncloudClient(): $e") + } + } + } + + override suspend fun getOwncloudClient(): OwnCloudClient? = withContext(Dispatchers.IO) { + try { + clientFactory.getClientFor(user.toOwnCloudAccount(), context) + } catch (e: Exception) { + Log_OC.d(tag, "Exception caught getOwncloudClient(): $e") + null + } + } +} diff --git a/app/src/main/java/com/nextcloud/ui/ChooseAccountDialogFragment.kt b/app/src/main/java/com/nextcloud/ui/ChooseAccountDialogFragment.kt index f5b7793..b45dcd0 100644 --- a/app/src/main/java/com/nextcloud/ui/ChooseAccountDialogFragment.kt +++ b/app/src/main/java/com/nextcloud/ui/ChooseAccountDialogFragment.kt @@ -4,9 +4,8 @@ * @author Infomaniak Network SA * Copyright (C) 2020 Infomaniak Network SA * - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ - package com.nextcloud.ui import android.annotation.SuppressLint @@ -19,12 +18,14 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.di.Injectable import com.nextcloud.client.network.ClientFactory import com.nextcloud.utils.extensions.getParcelableArgument +import com.nextcloud.utils.mdm.MDMConfig import com.owncloud.android.R import com.owncloud.android.databinding.DialogChooseAccountBinding import com.owncloud.android.datamodel.FileDataStorageManager @@ -34,10 +35,10 @@ import com.owncloud.android.ui.activity.BaseActivity import com.owncloud.android.ui.activity.DrawerActivity import com.owncloud.android.ui.adapter.UserListAdapter import com.owncloud.android.ui.adapter.UserListItem -import com.owncloud.android.ui.asynctasks.RetrieveStatusAsyncTask import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.DisplayUtils.AvatarGenerationListener import com.owncloud.android.utils.theme.ViewThemeUtils +import kotlinx.coroutines.launch import javax.inject.Inject private const val ARG_CURRENT_USER_PARAM = "currentUser" @@ -55,7 +56,7 @@ class ChooseAccountDialogFragment : private var currentStatus: Status? = null private var _binding: DialogChooseAccountBinding? = null - private val binding get() = _binding!! + val binding get() = _binding!! @Inject lateinit var clientFactory: ClientFactory @@ -83,6 +84,7 @@ class ChooseAccountDialogFragment : return builder.create() } + @Suppress("LongMethod") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) accountManager = (activity as BaseActivity).userAccountManager @@ -120,8 +122,7 @@ class ChooseAccountDialogFragment : viewThemeUtils ) - // hide "add account" when no multi account - if (!resources.getBoolean(R.bool.multiaccount_support)) { + if (!MDMConfig.multiAccountSupport(requireContext())) { binding.addAccount.visibility = View.GONE } @@ -138,13 +139,23 @@ class ChooseAccountDialogFragment : (activity as DrawerActivity).openManageAccounts() } - binding.setStatus.setOnClickListener { - val setStatusDialog = SetStatusDialogFragment.newInstance(accountManager.user, currentStatus) + binding.onlineStatus.setOnClickListener { + val setStatusDialog = SetOnlineStatusBottomSheet(currentStatus) setStatusDialog.show((activity as DrawerActivity).supportFragmentManager, "fragment_set_status") dismiss() } + binding.statusMessage.setOnClickListener { + val setStatusMessageDialog = SetStatusMessageBottomSheet(accountManager.user, currentStatus) + setStatusMessageDialog.show( + (activity as DrawerActivity).supportFragmentManager, + "fragment_set_status_message" + ) + + dismiss() + } + val capability = FileDataStorageManager(user, context?.contentResolver) .getCapability(user) @@ -152,18 +163,31 @@ class ChooseAccountDialogFragment : binding.statusView.visibility = View.VISIBLE } - RetrieveStatusAsyncTask(user, this, clientFactory).execute() + loadAndSetUserStatus(user) } themeViews() } + private fun loadAndSetUserStatus(user: User) { + viewLifecycleOwner.lifecycleScope.launch { + val status = retrieveUserStatus(user, clientFactory) + + if (isAdded && !isDetached) { + val context = requireContext() + setStatus(status, context) + } + } + } + private fun themeViews() { viewThemeUtils.platform.themeDialogDivider(binding.separatorLine) viewThemeUtils.platform.themeDialog(binding.root) - viewThemeUtils.material.colorMaterialTextButton(binding.setStatus) - viewThemeUtils.dialog.colorDialogMenuText(binding.setStatus) + viewThemeUtils.material.colorMaterialTextButton(binding.onlineStatus) + viewThemeUtils.dialog.colorDialogMenuText(binding.onlineStatus) + viewThemeUtils.material.colorMaterialTextButton(binding.statusMessage) + viewThemeUtils.dialog.colorDialogMenuText(binding.statusMessage) viewThemeUtils.material.colorMaterialTextButton(binding.addAccount) viewThemeUtils.dialog.colorDialogMenuText(binding.addAccount) viewThemeUtils.material.colorMaterialTextButton(binding.manageAccounts) @@ -187,21 +211,18 @@ class ChooseAccountDialogFragment : */ companion object { @JvmStatic - fun newInstance(user: User) = - ChooseAccountDialogFragment().apply { - arguments = Bundle().apply { - putParcelable(ARG_CURRENT_USER_PARAM, user) - } + fun newInstance(user: User) = ChooseAccountDialogFragment().apply { + arguments = Bundle().apply { + putParcelable(ARG_CURRENT_USER_PARAM, user) } + } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return dialogView - } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = + dialogView - override fun shouldCallGeneratedCallback(tag: String?, callContext: Any?): Boolean { - return (callContext as ImageView).tag.toString() == tag - } + override fun shouldCallGeneratedCallback(tag: String?, callContext: Any?): Boolean = + (callContext as ImageView).tag.toString() == tag override fun avatarGenerated(avatarDrawable: Drawable?, callContext: Any?) { if (_binding != null) { @@ -227,7 +248,7 @@ class ChooseAccountDialogFragment : binding.currentAccount.status.let { if (newStatus.message.isNullOrBlank()) { - it.text = "" + it.text = getString(R.string.empty) it.visibility = View.GONE } else { it.text = newStatus.message diff --git a/app/src/main/java/com/nextcloud/ui/ChooseStorageLocationDialogFragment.kt b/app/src/main/java/com/nextcloud/ui/ChooseStorageLocationDialogFragment.kt new file mode 100644 index 0000000..fc9760f --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/ChooseStorageLocationDialogFragment.kt @@ -0,0 +1,170 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 ZetaTom <70907959+ZetaTom@users.noreply.github.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import android.preference.PreferenceManager +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.client.di.Injectable +import com.nextcloud.client.preferences.AppPreferencesImpl +import com.owncloud.android.MainApp +import com.owncloud.android.R +import com.owncloud.android.databinding.DialogDataStorageLocationBinding +import com.owncloud.android.datastorage.DataStorageProvider +import com.owncloud.android.datastorage.StoragePoint +import com.owncloud.android.datastorage.StoragePoint.PrivacyType +import com.owncloud.android.datastorage.StoragePoint.StorageType +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.theme.ViewThemeUtils +import java.io.File +import javax.inject.Inject + +class ChooseStorageLocationDialogFragment : + DialogFragment(), + Injectable { + + private lateinit var binding: DialogDataStorageLocationBinding + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + private val storagePoints = DataStorageProvider.getInstance().availableStoragePoints + + private val selectedStorageType + get() = if (!binding.storageExternalRadio.isChecked) StorageType.INTERNAL else StorageType.EXTERNAL + private val selectedPrivacyType + get() = if (binding.allowMediaIndexSwitch.isChecked) PrivacyType.PUBLIC else PrivacyType.PRIVATE + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + binding = DialogDataStorageLocationBinding.inflate(layoutInflater) + + viewThemeUtils.material.colorMaterialSwitch(binding.allowMediaIndexSwitch) + viewThemeUtils.platform.themeRadioButton(binding.storageInternalRadio) + viewThemeUtils.platform.themeRadioButton(binding.storageExternalRadio) + + val builder = MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.storage_choose_location) + .setPositiveButton(R.string.common_ok) { dialog: DialogInterface, _ -> + notifyResult() + dialog.dismiss() + }.setView(binding.root) + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(requireContext(), builder) + + binding.storageRadioGroup.setOnCheckedChangeListener { _, _ -> + updateMediaIndexSwitch() + } + + binding.allowMediaIndexSwitch.setOnCheckedChangeListener { _, _ -> + updateStorageTypeSelection() + } + + return builder.create() + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = + binding.root + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + setupLocationSelection() + super.onViewCreated(view, savedInstanceState) + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + activity?.finish() + } + + private fun setupLocationSelection() { + updateStorageTypeSelection() + val currentStorageLocation = getCurrentStorageLocation() ?: return + + val radioButton = when (currentStorageLocation.storageType) { + StorageType.EXTERNAL -> binding.storageExternalRadio + else -> binding.storageInternalRadio + } + + radioButton.isChecked = true + updateMediaIndexSwitch() + } + + private fun getStoragePointLabel(storageType: StorageType, privacyType: PrivacyType): String { + val typeString = when (storageType) { + StorageType.INTERNAL -> getString(R.string.storage_internal_storage) + StorageType.EXTERNAL -> getString(R.string.storage_external_storage) + } + + val storagePath = + storagePoints.find { it.storageType == storageType && it.privacyType == privacyType }?.path + + return storagePath?.let { + val file = File(it) + val totalSpace = file.totalSpace + val usedSpace = totalSpace - file.freeSpace + return String.format( + getString(R.string.file_migration_free_space), + typeString, + DisplayUtils.bytesToHumanReadable(usedSpace), + DisplayUtils.bytesToHumanReadable(totalSpace) + ) + } ?: typeString + } + + private fun updateMediaIndexSwitch() { + val privacyTypes = + storagePoints.filter { it.storageType == selectedStorageType }.map { it.privacyType }.distinct() + binding.allowMediaIndexSwitch.isEnabled = privacyTypes.size > 1 + binding.allowMediaIndexSwitch.isChecked = privacyTypes.contains(PrivacyType.PUBLIC) + } + + private fun updateStorageTypeSelection() { + val hasInternalStorage = storagePoints.any { it.storageType == StorageType.INTERNAL } + val hasExternalStorage = storagePoints.any { it.storageType == StorageType.EXTERNAL } + + binding.storageInternalRadio.isEnabled = hasInternalStorage + binding.storageInternalRadio.text = getStoragePointLabel(StorageType.INTERNAL, selectedPrivacyType) + + binding.storageExternalRadio.isEnabled = hasExternalStorage + binding.storageExternalRadio.text = getStoragePointLabel(StorageType.EXTERNAL, selectedPrivacyType) + } + + private fun getCurrentStorageLocation(): StoragePoint? { + val appContext = MainApp.getAppContext() + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(appContext) + val storagePath = sharedPreferences.getString(AppPreferencesImpl.STORAGE_PATH, appContext.filesDir.absolutePath) + return storagePoints.find { it.path == storagePath } + } + + private fun notifyResult() { + val newPath = + storagePoints.find { it.storageType == selectedStorageType && it.privacyType == selectedPrivacyType } + ?: return + + val resultBundle = Bundle().apply { + putString(KEY_RESULT_STORAGE_LOCATION, newPath.path) + } + + parentFragmentManager.setFragmentResult(KEY_RESULT_STORAGE_LOCATION, resultBundle) + } + + companion object { + const val KEY_RESULT_STORAGE_LOCATION = "KEY_RESULT_STORAGE_LOCATION" + const val STORAGE_LOCATION_RESULT_CODE = 100 + + @JvmStatic + fun newInstance() = ChooseStorageLocationDialogFragment() + + @JvmStatic + val TAG: String = Companion::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/ui/ClearStatusTask.kt b/app/src/main/java/com/nextcloud/ui/ClearStatusTask.kt index 03bd615..0c90f59 100644 --- a/app/src/main/java/com/nextcloud/ui/ClearStatusTask.kt +++ b/app/src/main/java/com/nextcloud/ui/ClearStatusTask.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.ui @@ -15,15 +15,13 @@ import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.users.ClearStatusMessageRemoteOperation public class ClearStatusTask(val account: Account?, val context: Context?) : Function0 { - override fun invoke(): Boolean { - return try { - val client = OwnCloudClientFactory.createNextcloudClient(account, context) + override fun invoke(): Boolean = try { + val client = OwnCloudClientFactory.createNextcloudClient(account, context) - ClearStatusMessageRemoteOperation().execute(client).isSuccess - } catch (e: AccountUtils.AccountNotFoundException) { - Log_OC.e(this, "Error clearing status", e) + ClearStatusMessageRemoteOperation().execute(client).isSuccess + } catch (e: AccountUtils.AccountNotFoundException) { + Log_OC.e(this, "Error clearing status", e) - false - } + false } } diff --git a/app/src/main/java/com/nextcloud/ui/ImageDetailFragment.kt b/app/src/main/java/com/nextcloud/ui/ImageDetailFragment.kt index 8fc1890..a0c2f73 100644 --- a/app/src/main/java/com/nextcloud/ui/ImageDetailFragment.kt +++ b/app/src/main/java/com/nextcloud/ui/ImageDetailFragment.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 ZetaTom * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.ui @@ -11,7 +11,6 @@ import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.graphics.drawable.LayerDrawable -import android.net.Uri import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater @@ -19,12 +18,14 @@ import android.view.View import android.view.ViewGroup import androidx.annotation.VisibleForTesting import androidx.core.content.ContextCompat +import androidx.core.net.toUri import androidx.fragment.app.Fragment import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.NominatimClient import com.nextcloud.client.account.User import com.nextcloud.client.di.Injectable import com.nextcloud.utils.extensions.getParcelableArgument +import com.nextcloud.utils.extensions.logFileSize import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.databinding.PreviewImageDetailsFragmentBinding @@ -53,7 +54,9 @@ import javax.inject.Inject import kotlin.math.pow import kotlin.math.roundToInt -class ImageDetailFragment : Fragment(), Injectable { +class ImageDetailFragment : + Fragment(), + Injectable { private lateinit var binding: PreviewImageDetailsFragmentBinding private lateinit var file: OCFile private lateinit var user: User @@ -63,6 +66,8 @@ class ImageDetailFragment : Fragment(), Injectable { @Inject lateinit var viewThemeUtils: ViewThemeUtils + private val tag = "ImageDetailFragment" + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = PreviewImageDetailsFragmentBinding.inflate(layoutInflater, container, false) @@ -93,7 +98,8 @@ class ImageDetailFragment : Fragment(), Injectable { } nominatimClient = NominatimClient( - getString(R.string.osm_geocoder_url), getString(R.string.osm_geocoder_contact) + getString(R.string.osm_geocoder_url), + getString(R.string.osm_geocoder_contact) ) return binding.root @@ -101,6 +107,7 @@ class ImageDetailFragment : Fragment(), Injectable { override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) + file.logFileSize(tag) outState.putParcelable(ARG_FILE, file) outState.putParcelable(ARG_USER, user) outState.putParcelable(ARG_METADATA, metadata) @@ -323,14 +330,12 @@ class ImageDetailFragment : Fragment(), Injectable { } @SuppressLint("SimpleDateFormat") - private fun formatDate(timestamp: Long): String { - return buildString { - append(SimpleDateFormat("EEEE").format(timestamp)) - append(TEXT_SEP) - append(DateFormat.getDateInstance(DateFormat.MEDIUM).format(timestamp)) - append(TEXT_SEP) - append(DateFormat.getTimeInstance(DateFormat.SHORT).format(timestamp)) - } + private fun formatDate(timestamp: Long): String = buildString { + append(SimpleDateFormat("EEEE").format(timestamp)) + append(TEXT_SEP) + append(DateFormat.getDateInstance(DateFormat.MEDIUM).format(timestamp)) + append(TEXT_SEP) + append(DateFormat.getTimeInstance(DateFormat.SHORT).format(timestamp)) } private fun imagePinDrawable(context: Context): LayerDrawable { @@ -351,14 +356,12 @@ class ImageDetailFragment : Fragment(), Injectable { private fun markerOnGestureListener(latitude: Double, longitude: Double) = object : OnItemGestureListener { override fun onItemSingleTapUp(index: Int, item: OverlayItem): Boolean { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse("geo:0,0?q=$latitude,$longitude")) + val intent = Intent(Intent.ACTION_VIEW, "geo:0,0?q=$latitude,$longitude".toUri()) DisplayUtils.startIntentIfAppAvailable(intent, activity, R.string.no_map_app_availble) return true } - override fun onItemLongPress(index: Int, item: OverlayItem): Boolean { - return false - } + override fun onItemLongPress(index: Int, item: OverlayItem): Boolean = false } @Parcelize @@ -384,12 +387,10 @@ class ImageDetailFragment : Fragment(), Injectable { private const val SCROLL_LIMIT = 80.0 @JvmStatic - fun newInstance(file: OCFile, user: User): ImageDetailFragment { - return ImageDetailFragment().apply { - arguments = Bundle().apply { - putParcelable(ARG_FILE, file) - putParcelable(ARG_USER, user) - } + fun newInstance(file: OCFile, user: User): ImageDetailFragment = ImageDetailFragment().apply { + arguments = Bundle().apply { + putParcelable(ARG_FILE, file) + putParcelable(ARG_USER, user) } } } diff --git a/app/src/main/java/com/nextcloud/ui/RetrieveStatus.kt b/app/src/main/java/com/nextcloud/ui/RetrieveStatus.kt new file mode 100644 index 0000000..8231f07 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/RetrieveStatus.kt @@ -0,0 +1,35 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Edvard Holst + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui + +import com.nextcloud.client.account.User +import com.nextcloud.client.network.ClientFactory +import com.owncloud.android.lib.resources.users.GetStatusRemoteOperation +import com.owncloud.android.lib.resources.users.Status +import com.owncloud.android.lib.resources.users.StatusType +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.IOException + +suspend fun retrieveUserStatus(user: User, clientFactory: ClientFactory): Status = withContext(Dispatchers.IO) { + try { + val client = clientFactory.createNextcloudClient(user) + val result = GetStatusRemoteOperation().execute(client) + if (result.isSuccess && result.resultData is Status) { + result.resultData as Status + } else { + offlineStatus() + } + } catch (e: ClientFactory.CreationException) { + offlineStatus() + } catch (e: IOException) { + offlineStatus() + } +} + +private fun offlineStatus() = Status(StatusType.OFFLINE, "", "", -1) diff --git a/app/src/main/java/com/nextcloud/ui/SetOnlineStatusBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/SetOnlineStatusBottomSheet.kt new file mode 100644 index 0000000..f27ef8c --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/SetOnlineStatusBottomSheet.kt @@ -0,0 +1,160 @@ +/* + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2020 Nextcloud GmbH + * + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ + +package com.nextcloud.ui + +import android.annotation.SuppressLint +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.card.MaterialCardView +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.core.AsyncRunner +import com.nextcloud.client.di.Injectable +import com.nextcloud.utils.extensions.setVisibleIf +import com.owncloud.android.R +import com.owncloud.android.databinding.SetOnlineStatusBottomSheetBinding +import com.owncloud.android.lib.resources.users.Status +import com.owncloud.android.lib.resources.users.StatusType +import com.owncloud.android.ui.activity.BaseActivity +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.theme.CapabilityUtils +import com.owncloud.android.utils.theme.ViewThemeUtils +import javax.inject.Inject + +class SetOnlineStatusBottomSheet(val currentStatus: Status?) : + BottomSheetDialogFragment(R.layout.set_online_status_bottom_sheet), + Injectable { + + private lateinit var binding: SetOnlineStatusBottomSheetBinding + + private lateinit var accountManager: UserAccountManager + + @Inject + lateinit var asyncRunner: AsyncRunner + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @SuppressLint("DefaultLocale") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + accountManager = (activity as BaseActivity).userAccountManager + + currentStatus?.let { + updateCurrentStatusViews(it) + } + + binding.onlineStatus.setOnClickListener { setStatus(StatusType.ONLINE) } + binding.awayStatus.setOnClickListener { setStatus(StatusType.AWAY) } + binding.busyStatus.setOnClickListener { setStatus(StatusType.BUSY) } + binding.dndStatus.setOnClickListener { setStatus(StatusType.DND) } + binding.invisibleStatus.setOnClickListener { setStatus(StatusType.INVISIBLE) } + + viewThemeUtils.files.themeStatusCardView(binding.onlineStatus) + viewThemeUtils.files.themeStatusCardView(binding.awayStatus) + viewThemeUtils.files.themeStatusCardView(binding.busyStatus) + viewThemeUtils.files.themeStatusCardView(binding.dndStatus) + viewThemeUtils.files.themeStatusCardView(binding.invisibleStatus) + + viewThemeUtils.platform.themeDialog(binding.root) + + binding.busyStatus.setVisibleIf(CapabilityUtils.getCapability(context).userStatusSupportsBusy.isTrue) + } + + private fun updateCurrentStatusViews(it: Status) { + visualizeStatus(it.status) + } + + private fun setStatus(statusType: StatusType) { + asyncRunner.postQuickTask( + SetStatusTask( + statusType, + accountManager.currentOwnCloudAccount?.savedAccount, + context + ), + { + if (it) { + dismiss() + } else { + showErrorSnackbar() + } + }, + { + showErrorSnackbar() + } + ) + } + + private fun showErrorSnackbar() { + DisplayUtils.showSnackMessage(view, "Failed to set status!") + clearTopStatus() + } + + private fun visualizeStatus(statusType: StatusType) { + clearTopStatus() + val views: Triple = when (statusType) { + StatusType.ONLINE -> Triple(binding.onlineStatus, binding.onlineHeadline, binding.onlineIcon) + StatusType.AWAY -> Triple(binding.awayStatus, binding.awayHeadline, binding.awayIcon) + StatusType.BUSY -> Triple(binding.busyStatus, binding.busyHeadline, binding.busyIcon) + StatusType.DND -> Triple(binding.dndStatus, binding.dndHeadline, binding.dndIcon) + StatusType.INVISIBLE -> Triple(binding.invisibleStatus, binding.invisibleHeadline, binding.invisibleIcon) + else -> { + Log.d(TAG, "unknown status") + return + } + } + views.first.isChecked = true + viewThemeUtils.platform.colorOnSecondaryContainerTextViewElement(views.second) + } + + private fun clearTopStatus() { + context?.let { + binding.onlineHeadline.setTextColor( + resources.getColor(com.nextcloud.android.common.ui.R.color.high_emphasis_text) + ) + binding.awayHeadline.setTextColor( + resources.getColor(com.nextcloud.android.common.ui.R.color.high_emphasis_text) + ) + binding.busyHeadline.setTextColor( + resources.getColor(com.nextcloud.android.common.ui.R.color.high_emphasis_text) + ) + binding.dndHeadline.setTextColor( + resources.getColor(com.nextcloud.android.common.ui.R.color.high_emphasis_text) + ) + binding.invisibleHeadline.setTextColor( + resources.getColor(com.nextcloud.android.common.ui.R.color.high_emphasis_text) + ) + + binding.awayIcon.imageTintList = null + binding.dndIcon.imageTintList = null + binding.invisibleIcon.imageTintList = null + + binding.onlineStatus.isChecked = false + binding.awayStatus.isChecked = false + binding.busyStatus.isChecked = false + binding.dndStatus.isChecked = false + binding.invisibleStatus.isChecked = false + } + } + + companion object { + private val TAG = SetOnlineStatusBottomSheet::class.simpleName + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = SetOnlineStatusBottomSheetBinding.inflate(layoutInflater, container, false) + return binding.root + } +} diff --git a/app/src/main/java/com/nextcloud/ui/SetPredefinedCustomStatusTask.kt b/app/src/main/java/com/nextcloud/ui/SetPredefinedCustomStatusTask.kt index 8111280..2cb5b5e 100644 --- a/app/src/main/java/com/nextcloud/ui/SetPredefinedCustomStatusTask.kt +++ b/app/src/main/java/com/nextcloud/ui/SetPredefinedCustomStatusTask.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.ui @@ -20,15 +20,13 @@ class SetPredefinedCustomStatusTask( val account: Account?, val context: Context? ) : Function0 { - override fun invoke(): Boolean { - return try { - val client = OwnCloudClientFactory.createNextcloudClient(account, context) + override fun invoke(): Boolean = try { + val client = OwnCloudClientFactory.createNextcloudClient(account, context) - SetPredefinedCustomStatusMessageRemoteOperation(messageId, clearAt).execute(client).isSuccess - } catch (e: AccountUtils.AccountNotFoundException) { - Log_OC.e(this, "Error setting predefined status", e) + SetPredefinedCustomStatusMessageRemoteOperation(messageId, clearAt).execute(client).isSuccess + } catch (e: AccountUtils.AccountNotFoundException) { + Log_OC.e(this, "Error setting predefined status", e) - false - } + false } } diff --git a/app/src/main/java/com/nextcloud/ui/SetStatusDialogFragment.kt b/app/src/main/java/com/nextcloud/ui/SetStatusMessageBottomSheet.kt similarity index 69% rename from app/src/main/java/com/nextcloud/ui/SetStatusDialogFragment.kt rename to app/src/main/java/com/nextcloud/ui/SetStatusMessageBottomSheet.kt index dd12438..0215388 100644 --- a/app/src/main/java/com/nextcloud/ui/SetStatusDialogFragment.kt +++ b/app/src/main/java/com/nextcloud/ui/SetStatusMessageBottomSheet.kt @@ -4,16 +4,14 @@ * @author Tobias Kaminsky * Copyright (C) 2020 Nextcloud GmbH * - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.ui import android.annotation.SuppressLint -import android.app.Dialog import android.content.Context import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -21,28 +19,21 @@ import android.view.inputmethod.InputMethodManager import android.widget.AdapterView import android.widget.AdapterView.OnItemSelectedListener import android.widget.ArrayAdapter -import android.widget.ImageView -import android.widget.TextView import androidx.annotation.VisibleForTesting -import androidx.fragment.app.DialogFragment import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.card.MaterialCardView -import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.gson.Gson import com.google.gson.reflect.TypeToken import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.core.AsyncRunner import com.nextcloud.client.di.Injectable -import com.nextcloud.client.network.ClientFactory -import com.nextcloud.utils.extensions.getParcelableArgument import com.owncloud.android.R -import com.owncloud.android.databinding.DialogSetStatusBinding +import com.owncloud.android.databinding.SetStatusMessageBottomSheetBinding import com.owncloud.android.datamodel.ArbitraryDataProvider import com.owncloud.android.lib.resources.users.ClearAt import com.owncloud.android.lib.resources.users.PredefinedStatus import com.owncloud.android.lib.resources.users.Status -import com.owncloud.android.lib.resources.users.StatusType import com.owncloud.android.ui.activity.BaseActivity import com.owncloud.android.ui.adapter.PredefinedStatusClickListener import com.owncloud.android.ui.adapter.PredefinedStatusListAdapter @@ -57,9 +48,6 @@ import java.util.Calendar import java.util.Locale import javax.inject.Inject -private const val ARG_CURRENT_USER_PARAM = "currentUser" -private const val ARG_CURRENT_STATUS_PARAM = "currentStatus" - private const val POS_DONT_CLEAR = 0 private const val POS_HALF_AN_HOUR = 1 private const val POS_AN_HOUR = 2 @@ -78,15 +66,13 @@ private const val LAST_SECOND_OF_MINUTE = 59 private const val CLEAR_AT_TYPE_PERIOD = "period" private const val CLEAR_AT_TYPE_END_OF = "end-of" -class SetStatusDialogFragment : - DialogFragment(), +class SetStatusMessageBottomSheet(val user: User, val currentStatus: Status?) : + BottomSheetDialogFragment(R.layout.set_status_message_bottom_sheet), PredefinedStatusClickListener, Injectable { - private lateinit var binding: DialogSetStatusBinding + private lateinit var binding: SetStatusMessageBottomSheetBinding - private var currentUser: User? = null - private var currentStatus: Status? = null private lateinit var accountManager: UserAccountManager private lateinit var predefinedStatus: ArrayList private lateinit var adapter: PredefinedStatusListAdapter @@ -100,40 +86,22 @@ class SetStatusDialogFragment : @Inject lateinit var asyncRunner: AsyncRunner - @Inject - lateinit var clientFactory: ClientFactory - @Inject lateinit var viewThemeUtils: ViewThemeUtils override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - arguments?.let { - currentUser = it.getParcelableArgument(ARG_CURRENT_USER_PARAM, User::class.java) - currentStatus = it.getParcelableArgument(ARG_CURRENT_STATUS_PARAM, Status::class.java) - val json = arbitraryDataProvider.getValue(currentUser, ArbitraryDataProvider.PREDEFINED_STATUS) + val json = arbitraryDataProvider.getValue(user, ArbitraryDataProvider.PREDEFINED_STATUS) - if (json.isNotEmpty()) { - val myType = object : TypeToken>() {}.type - predefinedStatus = Gson().fromJson(json, myType) - } + if (json.isNotEmpty()) { + val myType = object : TypeToken>() {}.type + predefinedStatus = Gson().fromJson(json, myType) } EmojiManager.install(GoogleEmojiProvider()) } - @SuppressLint("InflateParams") - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - binding = DialogSetStatusBinding.inflate(layoutInflater) - - val builder = MaterialAlertDialogBuilder(requireContext()).setView(binding.root) - - viewThemeUtils.dialog.colorMaterialAlertDialogBackground(binding.statusView.context, builder) - - return builder.create() - } - @SuppressLint("DefaultLocale") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -150,16 +118,6 @@ class SetStatusDialogFragment : binding.predefinedStatusList.adapter = adapter binding.predefinedStatusList.layoutManager = LinearLayoutManager(context) - binding.onlineStatus.setOnClickListener { setStatus(StatusType.ONLINE) } - binding.dndStatus.setOnClickListener { setStatus(StatusType.DND) } - binding.awayStatus.setOnClickListener { setStatus(StatusType.AWAY) } - binding.invisibleStatus.setOnClickListener { setStatus(StatusType.INVISIBLE) } - - viewThemeUtils.files.themeStatusCardView(binding.onlineStatus) - viewThemeUtils.files.themeStatusCardView(binding.dndStatus) - viewThemeUtils.files.themeStatusCardView(binding.awayStatus) - viewThemeUtils.files.themeStatusCardView(binding.invisibleStatus) - binding.clearStatus.setOnClickListener { clearStatus() } binding.setStatus.setOnClickListener { setStatusMessage() } binding.emoji.setOnClickListener { popup.show() } @@ -204,15 +162,19 @@ class SetStatusDialogFragment : } private fun updateCurrentStatusViews(it: Status) { - binding.emoji.setText(it.icon) + if (it.icon.isNullOrBlank()) { + binding.emoji.setText("😀") + } else { + binding.emoji.setText(it.icon) + } + binding.customStatusInput.text?.clear() binding.customStatusInput.setText(it.message) - visualizeStatus(it.status) if (it.clearAt > 0) { binding.clearStatusAfterSpinner.visibility = View.GONE binding.remainingClearTime.apply { - binding.clearStatusMessageTextView.text = getString(R.string.clear_status_message) + binding.clearStatusMessageTextView.text = getString(R.string.clear) visibility = View.VISIBLE text = DisplayUtils.getRelativeTimestamp(context, it.clearAt * ONE_SECOND_IN_MILLIS, true) .toString() @@ -220,7 +182,7 @@ class SetStatusDialogFragment : setOnClickListener { visibility = View.GONE binding.clearStatusAfterSpinner.visibility = View.VISIBLE - binding.clearStatusMessageTextView.text = getString(R.string.clear_status_message_after) + binding.clearStatusMessageTextView.text = getString(R.string.clear_status_after) } } } @@ -233,20 +195,24 @@ class SetStatusDialogFragment : // 30 minutes System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + THIRTY_MINUTES * ONE_MINUTE_IN_SECONDS } + POS_AN_HOUR -> { // one hour System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + ONE_MINUTE_IN_SECONDS * ONE_MINUTE_IN_SECONDS } + POS_FOUR_HOURS -> { // four hours System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + FOUR_HOURS * ONE_MINUTE_IN_SECONDS * ONE_MINUTE_IN_SECONDS } + POS_TODAY -> { // today val date = getLastSecondOfToday() dateToSeconds(date) } + POS_END_OF_WEEK -> { // end of week val date = getLastSecondOfToday() @@ -255,6 +221,7 @@ class SetStatusDialogFragment : } dateToSeconds(date) } + else -> clearAt } } @@ -263,10 +230,12 @@ class SetStatusDialogFragment : clearAt?.type == CLEAR_AT_TYPE_PERIOD -> { System.currentTimeMillis() / ONE_SECOND_IN_MILLIS + clearAt.time.toLong() } + clearAt?.type == CLEAR_AT_TYPE_END_OF && clearAt.time == "day" -> { val date = getLastSecondOfToday() dateToSeconds(date) } + else -> -1 } @@ -288,59 +257,6 @@ class SetStatusDialogFragment : ) } - private fun setStatus(statusType: StatusType) { - visualizeStatus(statusType) - - asyncRunner.postQuickTask( - SetStatusTask( - statusType, - accountManager.currentOwnCloudAccount?.savedAccount, - context - ), - { - if (!it) { - clearTopStatus() - } - }, - { clearTopStatus() } - ) - } - - private fun visualizeStatus(statusType: StatusType) { - clearTopStatus() - val views: Triple = when (statusType) { - StatusType.ONLINE -> Triple(binding.onlineStatus, binding.onlineHeadline, binding.onlineIcon) - StatusType.AWAY -> Triple(binding.awayStatus, binding.awayHeadline, binding.awayIcon) - StatusType.DND -> Triple(binding.dndStatus, binding.dndHeadline, binding.dndIcon) - StatusType.INVISIBLE -> Triple(binding.invisibleStatus, binding.invisibleHeadline, binding.invisibleIcon) - else -> { - Log.d(TAG, "unknown status") - return - } - } - views.first.isChecked = true - viewThemeUtils.platform.colorOnSecondaryContainerTextViewElement(views.second) - } - - private fun clearTopStatus() { - context?.let { - binding.onlineHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text)) - binding.awayHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text)) - binding.dndHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text)) - binding.invisibleHeadline.setTextColor(resources.getColor(R.color.high_emphasis_text)) - - binding.onlineIcon.imageTintList = null - binding.awayIcon.imageTintList = null - binding.dndIcon.imageTintList = null - binding.invisibleIcon.imageTintList = null - - binding.onlineStatus.isChecked = false - binding.awayStatus.isChecked = false - binding.dndStatus.isChecked = false - binding.invisibleStatus.isChecked = false - } - } - private fun setStatusMessage() { if (selectedPredefinedMessageId != null) { asyncRunner.postQuickTask( @@ -369,27 +285,13 @@ class SetStatusDialogFragment : private fun dismiss(boolean: Boolean) { if (boolean) { dismiss() - } - } - - /** - * Fragment creator - */ - companion object { - private val TAG = SetStatusDialogFragment::class.simpleName - - @JvmStatic - fun newInstance(user: User, status: Status?): SetStatusDialogFragment { - val args = Bundle() - args.putParcelable(ARG_CURRENT_USER_PARAM, user) - args.putParcelable(ARG_CURRENT_STATUS_PARAM, status) - val dialogFragment = SetStatusDialogFragment() - dialogFragment.arguments = args - return dialogFragment + } else { + DisplayUtils.showSnackMessage(view, view?.resources?.getString(R.string.error_setting_status_message)) } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = SetStatusMessageBottomSheetBinding.inflate(layoutInflater, container, false) return binding.root } @@ -402,7 +304,7 @@ class SetStatusDialogFragment : binding.remainingClearTime.visibility = View.GONE binding.clearStatusAfterSpinner.visibility = View.VISIBLE - binding.clearStatusMessageTextView.text = getString(R.string.clear_status_message_after) + binding.clearStatusMessageTextView.text = getString(R.string.clear_status_after) val clearAt = predefinedStatus.clearAt if (clearAt == null) { @@ -433,6 +335,7 @@ class SetStatusDialogFragment : } } + @SuppressLint("NotifyDataSetChanged") @VisibleForTesting fun setPredefinedStatus(predefinedStatus: ArrayList) { adapter.list = predefinedStatus diff --git a/app/src/main/java/com/nextcloud/ui/SetStatusTask.kt b/app/src/main/java/com/nextcloud/ui/SetStatusTask.kt index a7a2ae9..5477190 100644 --- a/app/src/main/java/com/nextcloud/ui/SetStatusTask.kt +++ b/app/src/main/java/com/nextcloud/ui/SetStatusTask.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.ui @@ -15,20 +15,14 @@ import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.users.SetStatusRemoteOperation import com.owncloud.android.lib.resources.users.StatusType -class SetStatusTask( - val statusType: StatusType, - val account: Account?, - val context: Context? -) : Function0 { - override fun invoke(): Boolean { - return try { - val client = OwnCloudClientFactory.createNextcloudClient(account, context) +class SetStatusTask(val statusType: StatusType, val account: Account?, val context: Context?) : Function0 { + override fun invoke(): Boolean = try { + val client = OwnCloudClientFactory.createNextcloudClient(account, context) - SetStatusRemoteOperation(statusType).execute(client).isSuccess - } catch (e: AccountUtils.AccountNotFoundException) { - Log_OC.e(this, "Error setting status", e) + SetStatusRemoteOperation(statusType).execute(client).isSuccess + } catch (e: AccountUtils.AccountNotFoundException) { + Log_OC.e(this, "Error setting status", e) - false - } + false } } diff --git a/app/src/main/java/com/nextcloud/ui/SetUserDefinedCustomStatusTask.kt b/app/src/main/java/com/nextcloud/ui/SetUserDefinedCustomStatusTask.kt index f4cf104..a35f01c 100644 --- a/app/src/main/java/com/nextcloud/ui/SetUserDefinedCustomStatusTask.kt +++ b/app/src/main/java/com/nextcloud/ui/SetUserDefinedCustomStatusTask.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.ui diff --git a/app/src/main/java/com/nextcloud/ui/SquareLoaderImageView.kt b/app/src/main/java/com/nextcloud/ui/SquareLoaderImageView.kt index d7bbccc..4395748 100644 --- a/app/src/main/java/com/nextcloud/ui/SquareLoaderImageView.kt +++ b/app/src/main/java/com/nextcloud/ui/SquareLoaderImageView.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2020 Andy Scherzinger - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.ui diff --git a/app/src/main/java/com/nextcloud/ui/behavior/OnScrollBehavior.kt b/app/src/main/java/com/nextcloud/ui/behavior/OnScrollBehavior.kt new file mode 100644 index 0000000..96dedbb --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/behavior/OnScrollBehavior.kt @@ -0,0 +1,36 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.ui.behavior + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.coordinatorlayout.widget.CoordinatorLayout +import com.google.android.material.behavior.HideViewOnScrollBehavior + +class OnScrollBehavior @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + HideViewOnScrollBehavior(context, attrs) { + + override fun onNestedScroll( + coordinatorLayout: CoordinatorLayout, + child: V, + target: View, + dxConsumed: Int, + dyConsumed: Int, + dxUnconsumed: Int, + dyUnconsumed: Int, + type: Int, + consumed: IntArray + ) { + if (dyConsumed > 0) { + slideOut(child) + } else if (dyConsumed < 0 || dyUnconsumed < 0) { + slideIn(child) + } + } +} diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt index f0de1e0..61cd04e 100644 --- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt +++ b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeActivity.kt @@ -1,13 +1,12 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2024 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.ui.composeActivity -import android.content.Context import android.os.Bundle import android.view.MenuItem import androidx.compose.material3.MaterialTheme @@ -19,29 +18,22 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import com.nextcloud.client.assistant.AssistantScreen import com.nextcloud.client.assistant.AssistantViewModel -import com.nextcloud.client.assistant.repository.AssistantRepository +import com.nextcloud.client.assistant.repository.local.AssistantLocalRepositoryImpl +import com.nextcloud.client.assistant.repository.remote.AssistantRemoteRepositoryImpl +import com.nextcloud.client.database.NextcloudDatabase import com.nextcloud.common.NextcloudClient -import com.nextcloud.common.User import com.nextcloud.utils.extensions.getSerializableArgument import com.owncloud.android.R import com.owncloud.android.databinding.ActivityComposeBinding -import com.owncloud.android.lib.common.OwnCloudClientFactory -import com.owncloud.android.lib.common.accounts.AccountUtils -import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.ui.activity.DrawerActivity -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.lang.ref.WeakReference class ComposeActivity : DrawerActivity() { lateinit var binding: ActivityComposeBinding - private var menuItemId: Int = R.id.nav_all_files companion object { const val DESTINATION = "DESTINATION" const val TITLE = "TITLE" - const val MENU_ITEM = "MENU_ITEM" } override fun onCreate(savedInstanceState: Bundle?) { @@ -51,9 +43,8 @@ class ComposeActivity : DrawerActivity() { val destination = intent.getSerializableArgument(DESTINATION, ComposeDestination::class.java) val titleId = intent.getIntExtra(TITLE, R.string.empty) - menuItemId = intent.getIntExtra(MENU_ITEM, R.id.nav_all_files) - setupDrawer(menuItemId) + setupDrawer() setupToolbarShowOnlyMenuButtonAndTitle(getString(titleId)) { openDrawer() @@ -63,56 +54,46 @@ class ComposeActivity : DrawerActivity() { MaterialTheme( colorScheme = viewThemeUtils.getColorScheme(this), content = { - Content(destination, storageManager.user, this) + Content(destination) } ) } } - override fun onResume() { - super.onResume() - setDrawerMenuItemChecked(menuItemId) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - android.R.id.home -> { - toggleDrawer() - true - } - else -> super.onOptionsItemSelected(item) + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + android.R.id.home -> { + toggleDrawer() + true } + else -> super.onOptionsItemSelected(item) } @Composable - private fun Content(destination: ComposeDestination?, user: User, context: Context) { + private fun Content(destination: ComposeDestination?) { var nextcloudClient by remember { mutableStateOf(null) } LaunchedEffect(Unit) { - nextcloudClient = getNextcloudClient(user, context) + nextcloudClient = clientRepository.getNextcloudClient() } if (destination == ComposeDestination.AssistantScreen) { + binding.bottomNavigation.menu.findItem(R.id.nav_assistant).run { + isChecked = true + } + + val dao = NextcloudDatabase.instance().assistantDao() + nextcloudClient?.let { client -> AssistantScreen( viewModel = AssistantViewModel( - repository = AssistantRepository(client), - context = WeakReference(this) + accountName = userAccountManager.user.accountName, + remoteRepository = AssistantRemoteRepositoryImpl(client, capabilities), + localRepository = AssistantLocalRepositoryImpl(dao) ), - activity = this + activity = this, + capability = capabilities ) } } } - - private suspend fun getNextcloudClient(user: User, context: Context): NextcloudClient? { - return withContext(Dispatchers.IO) { - try { - OwnCloudClientFactory.createNextcloudClient(user, context) - } catch (e: AccountUtils.AccountNotFoundException) { - Log_OC.e(this, "Error caught at init of createNextcloudClient", e) - null - } - } - } } diff --git a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt index e442a5c..10e80ad 100644 --- a/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt +++ b/app/src/main/java/com/nextcloud/ui/composeActivity/ComposeDestination.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2024 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.ui.composeActivity diff --git a/app/src/main/java/com/nextcloud/ui/composeComponents/alertDialog/SimpleAlertDialog.kt b/app/src/main/java/com/nextcloud/ui/composeComponents/alertDialog/SimpleAlertDialog.kt index 042f1c4..bcf724e 100644 --- a/app/src/main/java/com/nextcloud/ui/composeComponents/alertDialog/SimpleAlertDialog.kt +++ b/app/src/main/java/com/nextcloud/ui/composeComponents/alertDialog/SimpleAlertDialog.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2024 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.ui.composeComponents.alertDialog @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.material3.AlertDialog import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -41,6 +42,10 @@ fun SimpleAlertDialog( } AlertDialog( + containerColor = MaterialTheme.colorScheme.surface, + iconContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + textContentColor = MaterialTheme.colorScheme.onPrimaryContainer, onDismissRequest = { dismiss() }, title = { Text(text = title) diff --git a/app/src/main/java/com/nextcloud/ui/composeComponents/bottomSheet/MoreActionsBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/composeComponents/bottomSheet/MoreActionsBottomSheet.kt index ae5bb94..b3d409a 100644 --- a/app/src/main/java/com/nextcloud/ui/composeComponents/bottomSheet/MoreActionsBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/ui/composeComponents/bottomSheet/MoreActionsBottomSheet.kt @@ -1,8 +1,8 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.ui.composeComponents.bottomSheet @@ -37,16 +37,13 @@ import kotlinx.coroutines.launch @SuppressLint("ResourceAsColor") @OptIn(ExperimentalMaterial3Api::class) @Composable -fun MoreActionsBottomSheet( - title: String? = null, - actions: List Unit>>, - dismiss: () -> Unit -) { +fun MoreActionsBottomSheet(title: String? = null, actions: List Unit>>, dismiss: () -> Unit) { val sheetState = rememberModalBottomSheetState() val scope = rememberCoroutineScope() ModalBottomSheet( modifier = Modifier.padding(top = 32.dp), + containerColor = colorScheme.surface, onDismissRequest = { dismiss() }, @@ -76,8 +73,8 @@ fun MoreActionsBottomSheet( .launch { sheetState.hide() } .invokeOnCompletion { if (!sheetState.isVisible) { - action.third() dismiss() + action.third() } } } diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt b/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt index 6f20319..6e265a2 100644 --- a/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt +++ b/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt @@ -1,9 +1,10 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.ui.fileactions @@ -11,8 +12,13 @@ import androidx.annotation.DrawableRes import androidx.annotation.IdRes import androidx.annotation.StringRes import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile -enum class FileAction(@IdRes val id: Int, @StringRes val title: Int, @DrawableRes val icon: Int? = null) { +enum class FileAction( + @param:IdRes val id: Int, + @param:StringRes val title: Int, + @param:DrawableRes val icon: Int? = null +) { // selection SELECT_ALL(R.id.action_select_all_action_menu, R.string.select_all, R.drawable.ic_select_all), SELECT_NONE(R.id.action_deselect_all_action_menu, R.string.deselect_all, R.drawable.ic_select_none), @@ -21,19 +27,20 @@ enum class FileAction(@IdRes val id: Int, @StringRes val title: Int, @DrawableRe EDIT(R.id.action_edit, R.string.action_edit, R.drawable.ic_edit), SEE_DETAILS(R.id.action_see_details, R.string.actionbar_see_details, R.drawable.ic_information_outline), REMOVE_FILE(R.id.action_remove_file, R.string.common_remove, R.drawable.ic_delete), + LEAVE_SHARE(R.id.action_remove_file, R.string.common_leave_this_share, R.drawable.ic_cancel), // File moving RENAME_FILE(R.id.action_rename_file, R.string.common_rename, R.drawable.ic_rename), MOVE_OR_COPY(R.id.action_move_or_copy, R.string.actionbar_move_or_copy, R.drawable.ic_external), // favorites - FAVORITE(R.id.action_favorite, R.string.favorite, R.drawable.ic_star), - UNSET_FAVORITE(R.id.action_unset_favorite, R.string.unset_favorite, R.drawable.ic_star_outline), + FAVORITE(R.id.action_favorite, R.string.favorite, R.drawable.ic_star_outline), + UNSET_FAVORITE(R.id.action_unset_favorite, R.string.unset_favorite, R.drawable.ic_star), // Uploads and downloads DOWNLOAD_FILE(R.id.action_download_file, R.string.filedetails_download, R.drawable.ic_cloud_download), - SYNC_FILE(R.id.action_sync_file, R.string.filedetails_sync_file, R.drawable.ic_cloud_sync_on), - CANCEL_SYNC(R.id.action_cancel_sync, R.string.common_cancel_sync, R.drawable.ic_cloud_sync_off), + DOWNLOAD_FOLDER(R.id.action_sync_file, R.string.filedetails_sync_file, R.drawable.ic_sync), + CANCEL_SYNC(R.id.action_cancel_sync, R.string.common_cancel_sync, R.drawable.ic_sync_off), // File sharing EXPORT_FILE(R.id.action_export_file, R.string.filedetails_export, R.drawable.ic_export), @@ -52,37 +59,178 @@ enum class FileAction(@IdRes val id: Int, @StringRes val title: Int, @DrawableRe LOCK_FILE(R.id.action_lock_file, R.string.lock_file, R.drawable.ic_lock), // Shortcuts - PIN_TO_HOMESCREEN(R.id.action_pin_to_homescreen, R.string.pin_home, R.drawable.add_to_home_screen); + PIN_TO_HOMESCREEN(R.id.action_pin_to_homescreen, R.string.pin_home, R.drawable.add_to_home_screen), + + // Retry for offline operation + RETRY(R.id.action_retry, R.string.retry, R.drawable.ic_retry); companion object { /** * All file actions, in the order they should be displayed */ - @JvmField - val SORTED_VALUES = listOf( - UNLOCK_FILE, - EDIT, - FAVORITE, - UNSET_FAVORITE, - SEE_DETAILS, - LOCK_FILE, - RENAME_FILE, - MOVE_OR_COPY, - DOWNLOAD_FILE, - EXPORT_FILE, - STREAM_MEDIA, - SEND_SHARE_FILE, - SEND_FILE, - OPEN_FILE_WITH, - SYNC_FILE, - CANCEL_SYNC, - SELECT_ALL, - SELECT_NONE, - SET_ENCRYPTED, - UNSET_ENCRYPTED, - SET_AS_WALLPAPER, - REMOVE_FILE, - PIN_TO_HOMESCREEN - ) + fun getActions(files: Collection): List { + return mutableListOf( + UNLOCK_FILE, + EDIT, + FAVORITE, + UNSET_FAVORITE, + SEE_DETAILS, + LOCK_FILE, + RENAME_FILE, + MOVE_OR_COPY, + DOWNLOAD_FILE, + EXPORT_FILE, + STREAM_MEDIA, + SEND_SHARE_FILE, + SEND_FILE, + OPEN_FILE_WITH, + DOWNLOAD_FOLDER, + CANCEL_SYNC, + SELECT_ALL, + SELECT_NONE, + SET_ENCRYPTED, + UNSET_ENCRYPTED, + SET_AS_WALLPAPER, + PIN_TO_HOMESCREEN, + RETRY + ).apply { + val deleteOrLeaveShareAction = getDeleteOrLeaveShareAction(files) ?: return@apply + add(deleteOrLeaveShareAction) + } + } + + fun getFilePreviewActions(file: OCFile?): List { + val result = mutableSetOf( + R.id.action_rename_file, + R.id.action_sync_file, + R.id.action_move_or_copy, + R.id.action_favorite, + R.id.action_unset_favorite, + R.id.action_pin_to_homescreen + ) + + if (file != null) { + val actionsToHide = getActionsToHide(setOf(file)) + result.removeAll(actionsToHide) + } + + return result.toList() + } + + fun getFileDetailActions(file: OCFile?): List { + val result = mutableSetOf( + R.id.action_lock_file, + R.id.action_unlock_file, + R.id.action_edit, + R.id.action_favorite, + R.id.action_unset_favorite, + R.id.action_see_details, + R.id.action_move_or_copy, + R.id.action_stream_media, + R.id.action_send_share_file, + R.id.action_pin_to_homescreen + ) + + if (file?.isFolder == true) { + result.add(R.id.action_send_file) + result.add(R.id.action_sync_file) + } + + if (file?.isAPKorAAB == true) { + result.add(R.id.action_download_file) + result.add(R.id.action_export_file) + } + + if (file != null) { + val actionsToHide = getActionsToHide(setOf(file)) + result.removeAll(actionsToHide) + } + + return result.toList() + } + + fun getFileListActionsToHide(checkedFiles: Set): List { + val result = mutableSetOf() + + if (checkedFiles.any { it.isOfflineOperation }) { + result.addAll( + listOf( + R.id.action_favorite, + R.id.action_move_or_copy, + R.id.action_sync_file, + R.id.action_encrypted, + R.id.action_unset_encrypted, + R.id.action_edit, + R.id.action_download_file, + R.id.action_export_file, + R.id.action_set_as_wallpaper + ) + ) + } + + if (checkedFiles.any { it.isAPKorAAB }) { + result.addAll( + listOf( + R.id.action_send_share_file, + R.id.action_export_file, + R.id.action_sync_file, + R.id.action_download_file + ) + ) + } + + val actionsToHide = getActionsToHide(checkedFiles) + result.addAll(actionsToHide) + + return result.toList() + } + + fun getActionsToHide(files: Set): List { + if (files.isEmpty()) return emptyList() + + val result = mutableListOf() + + if (files.any { !it.canReshare() }) { + result.add(R.id.action_send_share_file) + } + + if (files.any { !it.canRename() }) { + result.add(R.id.action_rename_file) + } + + if (files.any { !it.canMove() }) { + result.add(R.id.action_move_or_copy) + } + + if (files.any { !it.canWrite() }) { + result.add(R.id.action_edit) + } + + if (files.any { it.isRecommendedFile }) { + val allowedForRecommended = setOf( + R.id.action_see_details, + R.id.action_set_as_wallpaper, + R.id.action_pin_to_homescreen, + R.id.action_open_file_with + ) + + val allActions = entries.map { it.id } + result.addAll(allActions - allowedForRecommended) + } + + return result + } + + private fun getDeleteOrLeaveShareAction(files: Collection): FileAction? { + if (files.any { !it.canDeleteOrLeaveShare() }) { + return null + } + + return if (files.any { it.isSharedWithMe }) { + LEAVE_SHARE + } else { + REMOVE_FILE + } + } } } diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt index 92acbdf..63ccd3b 100644 --- a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsBottomSheet.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.ui.fileactions @@ -33,6 +33,7 @@ import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.CurrentAccountProvider import com.nextcloud.client.di.Injectable import com.nextcloud.client.di.ViewModelFactory +import com.nextcloud.utils.extensions.setVisibleIf import com.owncloud.android.R import com.owncloud.android.databinding.FileActionsBottomSheetBinding import com.owncloud.android.databinding.FileActionsBottomSheetItemBinding @@ -44,10 +45,13 @@ import com.owncloud.android.lib.resources.files.model.FileLockType import com.owncloud.android.ui.activity.ComponentsGetter import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.DisplayUtils.AvatarGenerationListener +import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.theme.ViewThemeUtils import javax.inject.Inject -class FileActionsBottomSheet : BottomSheetDialogFragment(), Injectable { +class FileActionsBottomSheet : + BottomSheetDialogFragment(), + Injectable { @Inject lateinit var viewThemeUtils: ViewThemeUtils @@ -67,14 +71,14 @@ class FileActionsBottomSheet : BottomSheetDialogFragment(), Injectable { private lateinit var viewModel: FileActionsViewModel private var _binding: FileActionsBottomSheetBinding? = null - private val binding + val binding get() = _binding!! private lateinit var componentsGetter: ComponentsGetter private val thumbnailAsyncTasks = mutableListOf() - interface ResultListener { + fun interface ResultListener { fun onResult(@IdRes actionId: Int) } @@ -99,9 +103,7 @@ class FileActionsBottomSheet : BottomSheetDialogFragment(), Injectable { return binding.root } - private fun handleState( - state: FileActionsViewModel.UiState - ) { + private fun handleState(state: FileActionsViewModel.UiState) { toggleLoadingOrContent(state) when (state) { is FileActionsViewModel.UiState.LoadedForSingleFile -> { @@ -192,9 +194,7 @@ class FileActionsBottomSheet : BottomSheetDialogFragment(), Injectable { } } - private fun displayActions( - actions: List - ) { + private fun displayActions(actions: List) { if (binding.fileActionsList.isEmpty()) { actions.forEach { action -> val view = inflateActionView(action) @@ -206,11 +206,23 @@ class FileActionsBottomSheet : BottomSheetDialogFragment(), Injectable { private fun displayTitle(titleFile: OCFile?) { val decryptedFileName = titleFile?.decryptedFileName if (decryptedFileName != null) { - decryptedFileName.let { - binding.title.text = it + val isFolder = titleFile.isFolder + val isRTL = DisplayUtils.isRTL() + val (base, ext) = FileStorageUtils.getFilenameAndExtension(decryptedFileName, isFolder, isRTL) + val titleMaxWidth = DisplayUtils.convertDpToPixel( + requireContext().resources.configuration.screenWidthDp.times(FILENAME_MAX_WIDTH_PERCENTAGE).toFloat(), + context + ) + + binding.title.maxWidth = titleMaxWidth + binding.title.text = base + binding.extension.setVisibleIf(!isFolder) + if (!isFolder) { + binding.extension.text = ext } } else { binding.title.isVisible = false + binding.extension.isVisible = false } } @@ -238,9 +250,7 @@ class FileActionsBottomSheet : BottomSheetDialogFragment(), Injectable { icon.setImageDrawable(avatarDrawable) } - override fun shouldCallGeneratedCallback(tag: String?, callContext: Any?): Boolean { - return false - } + override fun shouldCallGeneratedCallback(tag: String?, callContext: Any?): Boolean = false } DisplayUtils.setAvatar( currentUserProvider.user, @@ -304,6 +314,7 @@ class FileActionsBottomSheet : BottomSheetDialogFragment(), Injectable { companion object { private const val REQUEST_KEY = "REQUEST_KEY_ACTION" private const val RESULT_KEY_ACTION_ID = "RESULT_KEY_ACTION_ID" + private const val FILENAME_MAX_WIDTH_PERCENTAGE = 0.6 @JvmStatic @JvmOverloads @@ -312,9 +323,7 @@ class FileActionsBottomSheet : BottomSheetDialogFragment(), Injectable { isOverflow: Boolean, @IdRes additionalToHide: List? = null - ): FileActionsBottomSheet { - return newInstance(1, listOf(file), isOverflow, additionalToHide, true) - } + ): FileActionsBottomSheet = newInstance(1, listOf(file), isOverflow, additionalToHide, true) @JvmStatic @JvmOverloads @@ -325,19 +334,17 @@ class FileActionsBottomSheet : BottomSheetDialogFragment(), Injectable { @IdRes additionalToHide: List? = null, inSingleFileFragment: Boolean = false - ): FileActionsBottomSheet { - return FileActionsBottomSheet().apply { - val argsBundle = bundleOf( - FileActionsViewModel.ARG_ALL_FILES_COUNT to numberOfAllFiles, - FileActionsViewModel.ARG_FILES to ArrayList(files), - FileActionsViewModel.ARG_IS_OVERFLOW to isOverflow, - FileActionsViewModel.ARG_IN_SINGLE_FILE_FRAGMENT to inSingleFileFragment - ) - additionalToHide?.let { - argsBundle.putIntArray(FileActionsViewModel.ARG_ADDITIONAL_FILTER, additionalToHide.toIntArray()) - } - arguments = argsBundle + ): FileActionsBottomSheet = FileActionsBottomSheet().apply { + val argsBundle = bundleOf( + FileActionsViewModel.ARG_ALL_FILES_COUNT to numberOfAllFiles, + FileActionsViewModel.ARG_FILES to ArrayList(files), + FileActionsViewModel.ARG_IS_OVERFLOW to isOverflow, + FileActionsViewModel.ARG_IN_SINGLE_FILE_FRAGMENT to inSingleFileFragment + ) + additionalToHide?.let { + argsBundle.putIntArray(FileActionsViewModel.ARG_ADDITIONAL_FILTER, additionalToHide.toIntArray()) } + arguments = argsBundle } } } diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsViewModel.kt b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsViewModel.kt index faf0c22..f42015b 100644 --- a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsViewModel.kt +++ b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionsViewModel.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.ui.fileactions @@ -28,8 +28,7 @@ class FileActionsViewModel @Inject constructor( private val currentAccountProvider: CurrentAccountProvider, private val filterFactory: FileMenuFilter.Factory, private val logger: Logger -) : - ViewModel() { +) : ViewModel() { data class LockInfo(val lockType: FileLockType, val lockedBy: String, val lockedUntil: Long?) @@ -54,10 +53,7 @@ class FileActionsViewModel @Inject constructor( @IdRes get() = _clickActionId - fun load( - arguments: Bundle, - componentsGetter: ComponentsGetter - ) { + fun load(arguments: Bundle, componentsGetter: ComponentsGetter) { val files: List? = arguments.getParcelableArrayList(ARG_FILES) val numberOfAllFiles: Int = arguments.getInt(ARG_ALL_FILES_COUNT, 1) val isOverflow = arguments.getBoolean(ARG_IS_OVERFLOW, false) @@ -82,7 +78,7 @@ class FileActionsViewModel @Inject constructor( ) { viewModelScope.launch(Dispatchers.IO) { val toHide = getHiddenActions(componentsGetter, numberOfAllFiles, files, isOverflow, inSingleFileFragment) - val availableActions = getActionsToShow(additionalFilter, toHide) + val availableActions = getActionsToShow(additionalFilter, toHide, files) updateStateLoaded(files, availableActions) } } @@ -93,28 +89,21 @@ class FileActionsViewModel @Inject constructor( files: Collection, isOverflow: Boolean?, inSingleFileFragment: Boolean - ): List { - return filterFactory.newInstance( - numberOfAllFiles ?: 1, - files.toList(), - componentsGetter, - isOverflow ?: false, - currentAccountProvider.user - ) - .getToHide(inSingleFileFragment) - } + ): List = filterFactory.newInstance( + numberOfAllFiles ?: 1, + files.toList(), + componentsGetter, + isOverflow ?: false, + currentAccountProvider.user + ) + .getToHide(inSingleFileFragment) - private fun getActionsToShow( - additionalFilter: IntArray?, - toHide: List - ) = FileAction.SORTED_VALUES - .filter { additionalFilter == null || it.id !in additionalFilter } - .filter { it.id !in toHide } + private fun getActionsToShow(additionalFilter: IntArray?, toHide: List, files: Collection) = + FileAction.getActions(files) + .filter { additionalFilter == null || it.id !in additionalFilter } + .filter { it.id !in toHide } - private fun updateStateLoaded( - files: Collection, - availableActions: List - ) { + private fun updateStateLoaded(files: Collection, availableActions: List) { val state: UiState = when (files.size) { 1 -> { val file = files.first() @@ -135,12 +124,10 @@ class FileActionsViewModel @Inject constructor( } } - private fun getLockedUntil(file: OCFile): Long? { - return if (file.lockTimestamp == 0L || file.lockTimeout == 0L) { - null - } else { - (file.lockTimestamp + file.lockTimeout) * TimeConstants.MILLIS_PER_SECOND - } + private fun getLockedUntil(file: OCFile): Long? = if (file.lockTimestamp == 0L || file.lockTimeout == 0L) { + null + } else { + (file.lockTimestamp + file.lockTimeout) * TimeConstants.MILLIS_PER_SECOND } fun onClick(action: FileAction) { diff --git a/app/src/main/java/com/nextcloud/ui/trashbinFileActions/TrashbinFileAction.kt b/app/src/main/java/com/nextcloud/ui/trashbinFileActions/TrashbinFileAction.kt new file mode 100644 index 0000000..9054e8f --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/trashbinFileActions/TrashbinFileAction.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 TSI-mc + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.nextcloud.ui.trashbinFileActions + +import androidx.annotation.DrawableRes +import androidx.annotation.IdRes +import androidx.annotation.StringRes +import com.owncloud.android.R + +enum class TrashbinFileAction(@IdRes val id: Int, @StringRes val title: Int, @DrawableRes val icon: Int? = null) { + DELETE_PERMANENTLY(R.id.action_delete, R.string.trashbin_file_remove, R.drawable.ic_delete), + RESTORE(R.id.restore, R.string.restore_item, R.drawable.ic_history), + SELECT_ALL(R.id.action_select_all_action_menu, R.string.select_all, R.drawable.ic_select_all), + SELECT_NONE(R.id.action_deselect_all_action_menu, R.string.deselect_all, R.drawable.ic_select_none); + + companion object { + /** + * All file actions, in the order they should be displayed + */ + @JvmField + val SORTED_VALUES = listOf( + DELETE_PERMANENTLY, + RESTORE, + SELECT_ALL, + SELECT_NONE + ) + } +} diff --git a/app/src/main/java/com/nextcloud/ui/trashbinFileActions/TrashbinFileActionsBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/trashbinFileActions/TrashbinFileActionsBottomSheet.kt new file mode 100644 index 0000000..b97c9ba --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/trashbinFileActions/TrashbinFileActionsBottomSheet.kt @@ -0,0 +1,234 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 TSI-mc + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.nextcloud.ui.trashbinFileActions + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.annotation.IdRes +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.os.bundleOf +import androidx.core.view.isEmpty +import androidx.core.view.isVisible +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.setFragmentResult +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModelProvider +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.client.account.CurrentAccountProvider +import com.nextcloud.client.di.Injectable +import com.nextcloud.client.di.ViewModelFactory +import com.nextcloud.utils.extensions.toOCFile +import com.owncloud.android.R +import com.owncloud.android.databinding.FileActionsBottomSheetBinding +import com.owncloud.android.databinding.FileActionsBottomSheetItemBinding +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.SyncedFolderProvider +import com.owncloud.android.datamodel.ThumbnailsCacheManager +import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.theme.ViewThemeUtils +import javax.inject.Inject + +class TrashbinFileActionsBottomSheet : + BottomSheetDialogFragment(), + Injectable { + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + @Inject + lateinit var vmFactory: ViewModelFactory + + @Inject + lateinit var currentUserProvider: CurrentAccountProvider + + @Inject + lateinit var storageManager: FileDataStorageManager + + @Inject + lateinit var syncedFolderProvider: SyncedFolderProvider + + private lateinit var viewModel: TrashbinFileActionsViewModel + + private var _binding: FileActionsBottomSheetBinding? = null + val binding + get() = _binding!! + + private val thumbnailAsyncTasks = mutableListOf() + + fun interface ResultListener { + fun onResult(@IdRes actionId: Int) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + viewModel = ViewModelProvider(this, vmFactory)[TrashbinFileActionsViewModel::class.java] + _binding = FileActionsBottomSheetBinding.inflate(inflater, container, false) + + viewModel.uiState.observe(viewLifecycleOwner, this::handleState) + + viewModel.clickActionId.observe(viewLifecycleOwner) { id -> + dispatchActionClick(id) + } + + viewModel.load(requireArguments()) + + val bottomSheetDialog = dialog as BottomSheetDialog + bottomSheetDialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED + bottomSheetDialog.behavior.skipCollapsed = true + + viewThemeUtils.platform.colorViewBackground(binding.bottomSheet, ColorRole.SURFACE) + + return binding.root + } + + private fun handleState(state: TrashbinFileActionsViewModel.UiState) { + toggleLoadingOrContent(state) + when (state) { + is TrashbinFileActionsViewModel.UiState.LoadedForSingleFile -> { + loadFileThumbnail(state.titleFile) + displayActions(state.actions) + displayTitle(state.titleFile) + } + + is TrashbinFileActionsViewModel.UiState.LoadedForMultipleFiles -> { + setMultipleFilesThumbnail() + displayActions(state.actions) + displayTitle(state.fileCount) + } + + TrashbinFileActionsViewModel.UiState.Loading -> {} + TrashbinFileActionsViewModel.UiState.Error -> { + context?.let { + Toast.makeText(it, R.string.error_file_actions, Toast.LENGTH_SHORT).show() + } + dismissAllowingStateLoss() + } + } + } + + private fun loadFileThumbnail(titleFile: TrashbinFile?) { + titleFile?.let { + DisplayUtils.setThumbnail( + it.toOCFile(), + binding.thumbnailLayout.thumbnail, + currentUserProvider.user, + storageManager, + thumbnailAsyncTasks, + false, + context, + binding.thumbnailLayout.thumbnailShimmer, + syncedFolderProvider.preferences, + viewThemeUtils, + syncedFolderProvider + ) + } + } + + private fun setMultipleFilesThumbnail() { + context?.let { + val drawable = viewThemeUtils.platform.tintDrawable(it, R.drawable.file_multiple, ColorRole.PRIMARY) + binding.thumbnailLayout.thumbnail.setImageDrawable(drawable) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + fun setResultListener( + fragmentManager: FragmentManager, + lifecycleOwner: LifecycleOwner, + listener: ResultListener + ): TrashbinFileActionsBottomSheet { + fragmentManager.setFragmentResultListener(REQUEST_KEY, lifecycleOwner) { _, result -> + @IdRes val actionId = result.getInt(RESULT_KEY_ACTION_ID, -1) + if (actionId != -1) { + listener.onResult(actionId) + } + } + return this + } + + private fun toggleLoadingOrContent(state: TrashbinFileActionsViewModel.UiState) { + if (state is TrashbinFileActionsViewModel.UiState.Loading) { + binding.bottomSheetLoading.isVisible = true + binding.bottomSheetHeader.isVisible = false + viewThemeUtils.platform.colorCircularProgressBar(binding.bottomSheetLoading, ColorRole.PRIMARY) + } else { + binding.bottomSheetLoading.isVisible = false + binding.bottomSheetHeader.isVisible = true + } + } + + private fun displayActions(actions: List) { + if (binding.fileActionsList.isEmpty()) { + actions.forEach { action -> + val view = inflateActionView(action) + binding.fileActionsList.addView(view) + } + } + } + + private fun displayTitle(titleFile: TrashbinFile?) { + titleFile?.fileName?.let { + binding.title.text = it + } ?: { binding.title.isVisible = false } + } + + private fun displayTitle(fileCount: Int) { + binding.title.text = resources.getQuantityString(R.plurals.trashbin_list__footer__file, fileCount, fileCount) + } + + private fun inflateActionView(action: TrashbinFileAction): View { + val itemBinding = FileActionsBottomSheetItemBinding.inflate(layoutInflater, binding.fileActionsList, false) + .apply { + root.setOnClickListener { + viewModel.onClick(action) + } + text.setText(action.title) + if (action.icon != null) { + val drawable = + viewThemeUtils.platform.tintDrawable( + requireContext(), + AppCompatResources.getDrawable(requireContext(), action.icon)!! + ) + icon.setImageDrawable(drawable) + } + } + return itemBinding.root + } + + private fun dispatchActionClick(id: Int?) { + if (id != null) { + setFragmentResult(REQUEST_KEY, bundleOf(RESULT_KEY_ACTION_ID to id)) + parentFragmentManager.clearFragmentResultListener(REQUEST_KEY) + dismiss() + } + } + + companion object { + private const val REQUEST_KEY = "REQUEST_KEY_ACTION" + private const val RESULT_KEY_ACTION_ID = "RESULT_KEY_ACTION_ID" + + @JvmStatic + fun newInstance(numberOfAllFiles: Int, files: Collection): TrashbinFileActionsBottomSheet = + TrashbinFileActionsBottomSheet().apply { + val argsBundle = bundleOf( + TrashbinFileActionsViewModel.ARG_ALL_FILES_COUNT to numberOfAllFiles, + TrashbinFileActionsViewModel.ARG_FILES to ArrayList(files) + ) + arguments = argsBundle + } + } +} diff --git a/app/src/main/java/com/nextcloud/ui/trashbinFileActions/TrashbinFileActionsViewModel.kt b/app/src/main/java/com/nextcloud/ui/trashbinFileActions/TrashbinFileActionsViewModel.kt new file mode 100644 index 0000000..1558978 --- /dev/null +++ b/app/src/main/java/com/nextcloud/ui/trashbinFileActions/TrashbinFileActionsViewModel.kt @@ -0,0 +1,96 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 TSI-mc + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.nextcloud.ui.trashbinFileActions + +import android.os.Bundle +import androidx.annotation.IdRes +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nextcloud.client.logger.Logger +import com.nextcloud.ui.fileactions.FileActionsViewModel +import com.owncloud.android.R +import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import javax.inject.Inject + +class TrashbinFileActionsViewModel @Inject constructor(private val logger: Logger) : ViewModel() { + + sealed interface UiState { + data object Loading : UiState + data object Error : UiState + data class LoadedForSingleFile(val actions: List, val titleFile: TrashbinFile?) : UiState + + data class LoadedForMultipleFiles(val actions: List, val fileCount: Int) : UiState + } + + private val _uiState: MutableLiveData = MutableLiveData(UiState.Loading) + val uiState: LiveData + get() = _uiState + + private val _clickActionId: MutableLiveData = MutableLiveData(null) + val clickActionId: LiveData + @IdRes + get() = _clickActionId + + fun load(arguments: Bundle) { + val files: List? = arguments.getParcelableArrayList(ARG_FILES) + val numberOfAllFiles: Int = arguments.getInt(FileActionsViewModel.ARG_ALL_FILES_COUNT, 1) + + if (files.isNullOrEmpty()) { + logger.d(TAG, "No valid files argument for loading actions") + _uiState.postValue(UiState.Error) + } else { + load(files.toList(), numberOfAllFiles) + } + } + + private fun load(files: Collection, numberOfAllFiles: Int?) { + viewModelScope.launch(Dispatchers.IO) { + val toHide = getHiddenActions(numberOfAllFiles, files) + val availableActions = getActionsToShow(toHide) + updateStateLoaded(files, availableActions) + } + } + + private fun getHiddenActions(numberOfAllFiles: Int?, files: Collection): List { + numberOfAllFiles?.let { + if (files.size >= it) { + return listOf(R.id.action_select_all_action_menu) + } + } + + return listOf() + } + + private fun getActionsToShow(toHide: List) = TrashbinFileAction.SORTED_VALUES.filter { it.id !in toHide } + + private fun updateStateLoaded(files: Collection, availableActions: List) { + val state: UiState = when (files.size) { + 1 -> { + val file = files.first() + UiState.LoadedForSingleFile(availableActions, file) + } + + else -> UiState.LoadedForMultipleFiles(availableActions, files.size) + } + _uiState.postValue(state) + } + + fun onClick(action: TrashbinFileAction) { + _clickActionId.value = action.id + } + + companion object { + const val ARG_ALL_FILES_COUNT = "ALL_FILES_COUNT" + const val ARG_FILES = "FILES" + + private val TAG = TrashbinFileActionsViewModel::class.simpleName!! + } +} diff --git a/app/src/main/java/com/nextcloud/utils/BitmapExtensions.kt b/app/src/main/java/com/nextcloud/utils/BitmapExtensions.kt new file mode 100644 index 0000000..545354c --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/BitmapExtensions.kt @@ -0,0 +1,160 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Matrix +import androidx.core.graphics.scale +import androidx.exifinterface.media.ExifInterface +import com.nextcloud.utils.extensions.toFile +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.utils.BitmapUtils.calculateSampleFactor + +private const val TAG = "BitmapExtension" + +@Suppress("MagicNumber") +fun Bitmap.allocationKilobyte(): Int = allocationByteCount.div(1024) + +/** + * Recursively scales down the Bitmap until its size allocation is within the specified size. + * + * This function checks if the current Bitmap's size (in kilobytes) is already within + * the target size. If not, it scales the Bitmap down by a factor of `1.5` in both width and height + * and calls itself recursively until the size condition is met. + * + * @receiver Bitmap The original Bitmap to be resized. + * @param targetKB The target size in kilobytes (KB) that the Bitmap should be reduced to. + * @return A scaled-down Bitmap that meets the size allocation requirement. + */ +@Suppress("MagicNumber") +fun Bitmap.scaleUntil(targetKB: Int): Bitmap { + if (allocationKilobyte() <= targetKB) { + return this + } + + // 1.5 is used to gradually scale down while minimizing distortion + val scaleRatio = 1.5 + val width = width.div(scaleRatio).toInt() + val height = height.div(scaleRatio).toInt() + + val scaledBitmap = scale(width, height) + return scaledBitmap.scaleUntil(targetKB) +} + +/** + * Rotates and/or flips a [Bitmap] according to an EXIF orientation constant. + * + * Needed because loading bitmaps directly may ignore EXIF metadata with some devices, + * resulting in incorrectly displayed images. + * + * This function uses a [Matrix] transformation to adjust the image so that it + * appears upright when displayed. It supports all standard EXIF orientations, + * including mirrored and rotated cases. + * + * The original bitmap will be recycled if a new one is successfully created. + * If the device runs out of memory during the transformation, the original bitmap + * is returned unchanged. + * + * @receiver The [Bitmap] to rotate or flip. Can be `null`. + * @param orientation One of the [ExifInterface] orientation constants, such as + * [ExifInterface.ORIENTATION_ROTATE_90] or [ExifInterface.ORIENTATION_FLIP_HORIZONTAL]. + * @return The correctly oriented [Bitmap], or `null` if the receiver was `null`. + * + * @see ExifInterface + * @see Matrix + */ +@Suppress("MagicNumber", "ReturnCount") +fun Bitmap?.rotateBitmapViaExif(orientation: Int): Bitmap? { + if (this == null) { + return null + } + + val matrix = Matrix() + when (orientation) { + ExifInterface.ORIENTATION_NORMAL -> return this + ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1f, 1f) + ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180f) + ExifInterface.ORIENTATION_FLIP_VERTICAL -> { + matrix.setRotate(180f) + matrix.postScale(-1f, 1f) + } + + ExifInterface.ORIENTATION_TRANSPOSE -> { + matrix.setRotate(90f) + matrix.postScale(-1f, 1f) + } + + ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90f) + ExifInterface.ORIENTATION_TRANSVERSE -> { + matrix.setRotate(-90f) + matrix.postScale(-1f, 1f) + } + + ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90f) + else -> return this + } + + return try { + val rotated = Bitmap.createBitmap( + this, + 0, + 0, + this.width, + this.height, + matrix, + true + ) + + // release original if a new one was created + if (rotated != this) { + this.recycle() + } + + rotated + } catch (_: OutOfMemoryError) { + Log_OC.e("BitmapExtension", "rotating bitmap, out of memory exception") + this + } +} + +/** + * Decodes a bitmap from a file path while minimizing memory usage. + * + * This function first checks if the file exists (via [toFile]), then performs following steps: + * + * 1. Reads image dimensions using [BitmapFactory.Options.inJustDecodeBounds] without allocating memory. + * 2. Calculates a sampling factor with [calculateSampleFactor] to scale down large images efficiently. + * 3. Decodes the actual bitmap using the computed sample size. + * + * @param srcPath Absolute path to the image file. + * @param reqWidth Desired width in pixels of the output bitmap. + * @param reqHeight Desired height in pixels of the output bitmap. + * @return The decoded [Bitmap], or `null` if the file does not exist or decoding fails. + */ +@Suppress("TooGenericExceptionCaught") +fun decodeSampledBitmapFromFile(srcPath: String?, reqWidth: Int, reqHeight: Int): Bitmap? { + // check existence of file + srcPath?.toFile() ?: return null + + // Read image dimensions without allocating memory just to get pixels + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeFile(srcPath, options) + + // Calculate sampling factor + options.inSampleSize = calculateSampleFactor(options, reqWidth, reqHeight) + options.inJustDecodeBounds = false + + // Decode actual bitmap + return try { + BitmapFactory.decodeFile(srcPath, options) + } catch (e: Exception) { + Log_OC.e(TAG, "exception during decoding path: $e") + null + } +} diff --git a/app/src/main/java/com/nextcloud/utils/BuildHelper.kt b/app/src/main/java/com/nextcloud/utils/BuildHelper.kt new file mode 100644 index 0000000..4eca26f --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/BuildHelper.kt @@ -0,0 +1,15 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Tobias Kaminsky + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.nextcloud.utils + +import com.owncloud.android.BuildConfig + +object BuildHelper { + const val GPLAY: String = "gplay" + + fun isFlavourGPlay(): Boolean = GPLAY == BuildConfig.FLAVOR +} diff --git a/app/src/main/java/com/nextcloud/utils/CalendarEventManager.kt b/app/src/main/java/com/nextcloud/utils/CalendarEventManager.kt new file mode 100644 index 0000000..a609071 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/CalendarEventManager.kt @@ -0,0 +1,78 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils + +import android.Manifest +import android.content.ContentUris +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.CalendarContract +import com.nextcloud.utils.extensions.showToast +import com.owncloud.android.R +import com.owncloud.android.lib.common.SearchResultEntry +import com.owncloud.android.ui.interfaces.UnifiedSearchListInterface +import com.owncloud.android.utils.PermissionUtil.checkSelfPermission + +class CalendarEventManager(private val context: Context) { + + fun openCalendarEvent(searchResult: SearchResultEntry, listInterface: UnifiedSearchListInterface) { + val havePermission = checkSelfPermission(context, Manifest.permission.READ_CALENDAR) + val createdAt = searchResult.createdAt() + val eventId: Long? = if (havePermission && createdAt != null) { + getCalendarEventId(searchResult.title, createdAt) + } else { + null + } + + if (eventId == null) { + val messageId = if (havePermission) { + R.string.unified_search_fragment_calendar_event_not_found + } else { + R.string.unified_search_fragment_permission_needed + } + context.showToast(messageId) + listInterface.onSearchResultClicked(searchResult) + } else { + val uri: Uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId) + val intent = Intent(Intent.ACTION_VIEW).setData(uri) + context.startActivity(intent) + } + } + + private fun getCalendarEventId(eventTitle: String, eventStartDate: Long): Long? { + val projection = arrayOf( + CalendarContract.Events._ID, + CalendarContract.Events.TITLE, + CalendarContract.Events.DTSTART + ) + + val selection = "${CalendarContract.Events.TITLE} = ? AND ${CalendarContract.Events.DTSTART} = ?" + val selectionArgs = arrayOf(eventTitle, eventStartDate.toString()) + + val cursor = context.contentResolver.query( + CalendarContract.Events.CONTENT_URI, + projection, + selection, + selectionArgs, + "${CalendarContract.Events.DTSTART} ASC" + ) + + cursor?.use { + if (cursor.moveToFirst()) { + val idIndex = cursor.getColumnIndex(CalendarContract.Events._ID) + return cursor.getLong(idIndex) + } + } + + return null + } +} + +@Suppress("MagicNumber") +private fun SearchResultEntry.createdAt(): Long? = attributes["createdAt"]?.toLongOrNull()?.times(1000L) diff --git a/app/src/main/java/com/nextcloud/utils/ContactManager.kt b/app/src/main/java/com/nextcloud/utils/ContactManager.kt new file mode 100644 index 0000000..c956e17 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/ContactManager.kt @@ -0,0 +1,144 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils + +import android.Manifest +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.ContactsContract +import com.nextcloud.utils.extensions.showToast +import com.owncloud.android.R +import com.owncloud.android.lib.common.SearchResultEntry +import com.owncloud.android.ui.interfaces.UnifiedSearchListInterface +import com.owncloud.android.utils.PermissionUtil.checkSelfPermission + +class ContactManager(private val context: Context) { + + fun openContact(searchResult: SearchResultEntry, listInterface: UnifiedSearchListInterface) { + val havePermission = checkSelfPermission(context, Manifest.permission.READ_CONTACTS) + val displayName = searchResult.displayName() + val contactId: Long? = if (havePermission && displayName != null) { + getContactIds(displayName).let { contactIds -> + if (contactIds.size > 1) getContactId(searchResult, contactIds) else contactIds.firstOrNull() + } + } else { + null + } + + if (contactId == null) { + val messageId = if (havePermission) { + R.string.unified_search_fragment_contact_not_found + } else { + R.string.unified_search_fragment_permission_needed + } + context.showToast(messageId) + listInterface.onSearchResultClicked(searchResult) + } else { + val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, contactId.toString()) + val intent = Intent(Intent.ACTION_VIEW).apply { + setData(uri) + } + context.startActivity(intent) + } + } + + private fun getContactId(searchResult: SearchResultEntry, contactIds: List): Long? { + val email = searchResult.email() + val phoneNumber = searchResult.phoneNumber() + + contactIds.forEach { + val targetEmail = getEmailById(it) ?: "" + val targetPhoneNumber = getPhoneNumberById(it) ?: "" + if (targetEmail == email && targetPhoneNumber == phoneNumber) { + return it + } + } + + return null + } + + private fun getEmailById(contactId: Long): String? { + var result: String? = null + val projection = arrayOf(ContactsContract.CommonDataKinds.Email.ADDRESS) + val selection = "${ContactsContract.CommonDataKinds.Email.CONTACT_ID} = ?" + val selectionArgs = arrayOf(contactId.toString()) + + val cursor = context.contentResolver.query( + ContactsContract.CommonDataKinds.Email.CONTENT_URI, + projection, + selection, + selectionArgs, + null + ) + + cursor?.use { + val emailIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS) + while (cursor.moveToNext()) { + result = cursor.getString(emailIndex) + } + } + + return result + } + + private fun getPhoneNumberById(contactId: Long): String? { + var result: String? = null + val projection = arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER) + val selection = "${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?" + val selectionArgs = arrayOf(contactId.toString()) + + val cursor = context.contentResolver.query( + ContactsContract.CommonDataKinds.Phone.CONTENT_URI, + projection, + selection, + selectionArgs, + null + ) + + cursor?.use { + val phoneIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER) + while (cursor.moveToNext()) { + result = cursor.getString(phoneIndex) + } + } + + return result + } + + private fun getContactIds(displayName: String): List { + val result = arrayListOf() + val projection = arrayOf(ContactsContract.Contacts._ID) + val selection = "${ContactsContract.Contacts.DISPLAY_NAME} = ?" + val selectionArgs = arrayOf(displayName) + + val cursor = context.contentResolver.query( + ContactsContract.Contacts.CONTENT_URI, + projection, + selection, + selectionArgs, + null + ) + + cursor?.use { + val idIndex = cursor.getColumnIndex(ContactsContract.Contacts._ID) + while (cursor.moveToNext()) { + val id = cursor.getLong(idIndex) + result.add(id) + } + } + + return result + } +} + +private fun SearchResultEntry.displayName(): String? = attributes["displayName"] + +private fun SearchResultEntry.email(): String? = attributes["email"] + +private fun SearchResultEntry.phoneNumber(): String? = attributes["phoneNumber"] diff --git a/app/src/main/java/com/nextcloud/utils/EditorUtils.kt b/app/src/main/java/com/nextcloud/utils/EditorUtils.kt index f969992..c45e14e 100644 --- a/app/src/main/java/com/nextcloud/utils/EditorUtils.kt +++ b/app/src/main/java/com/nextcloud/utils/EditorUtils.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.utils @@ -26,7 +26,5 @@ class EditorUtils @Inject constructor(private val arbitraryDataProvider: Arbitra ?: editors.firstOrNull { mimeType in it.optionalMimetypes } } - fun isEditorAvailable(user: User?, mimeType: String?): Boolean { - return getEditor(user, mimeType) != null - } + fun isEditorAvailable(user: User?, mimeType: String?): Boolean = getEditor(user, mimeType) != null } diff --git a/app/src/main/java/com/nextcloud/utils/FileHelper.kt b/app/src/main/java/com/nextcloud/utils/FileHelper.kt new file mode 100644 index 0000000..e417f88 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/FileHelper.kt @@ -0,0 +1,67 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils + +import com.owncloud.android.lib.common.utils.Log_OC +import java.io.File +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.util.stream.Collectors +import kotlin.io.path.pathString + +@Suppress("NestedBlockDepth") +object FileHelper { + private const val TAG = "FileHelper" + + fun listDirectoryEntries(directory: File?, startIndex: Int, maxItems: Int, fetchFolders: Boolean): List { + if (directory == null || !directory.exists() || !directory.isDirectory) return emptyList() + + return try { + Files.list(directory.toPath()) + .map { it.toFile() } + .filter { file -> if (fetchFolders) file.isDirectory else !file.isDirectory } + .skip(startIndex.toLong()) + .limit(maxItems.toLong()) + .collect(Collectors.toList()) + } catch (e: IOException) { + Log_OC.d(TAG, "listDirectoryEntries: $e") + emptyList() + } + } + + fun listFilesRecursive(files: Collection): List { + val result = mutableListOf() + + for (file in files) { + try { + collectFilesRecursively(file.toPath(), result) + } catch (e: IOException) { + Log_OC.e(TAG, "Error collecting files recursively from: ${file.absolutePath}", e) + } + } + + return result + } + + private fun collectFilesRecursively(path: Path, result: MutableList) { + if (Files.isDirectory(path)) { + try { + Files.newDirectoryStream(path).use { stream -> + for (entry in stream) { + collectFilesRecursively(entry, result) + } + } + } catch (e: IOException) { + Log_OC.e(TAG, "Error reading directory: ${path.pathString}", e) + } + } else { + result.add(path.pathString) + } + } +} diff --git a/app/src/main/java/com/nextcloud/utils/ForegroundServiceHelper.kt b/app/src/main/java/com/nextcloud/utils/ForegroundServiceHelper.kt index c01d644..c46c314 100644 --- a/app/src/main/java/com/nextcloud/utils/ForegroundServiceHelper.kt +++ b/app/src/main/java/com/nextcloud/utils/ForegroundServiceHelper.kt @@ -1,14 +1,15 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.utils import android.app.Notification import android.app.Service +import android.content.pm.ServiceInfo import android.os.Build import android.util.Log import androidx.core.app.ServiceCompat @@ -32,7 +33,7 @@ object ForegroundServiceHelper { service, id, notification, - foregroundServiceType.getId() + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC ) } catch (e: Exception) { Log.d(TAG, "Exception caught at ForegroundServiceHelper.startService: $e") @@ -46,11 +47,9 @@ object ForegroundServiceHelper { id: Int, notification: Notification, foregroundServiceType: ForegroundServiceType - ): ForegroundInfo { - return if (isAboveOrEqualAndroid10) { - ForegroundInfo(id, notification, foregroundServiceType.getId()) - } else { - ForegroundInfo(id, notification) - } + ): ForegroundInfo = if (isAboveOrEqualAndroid10) { + ForegroundInfo(id, notification, foregroundServiceType.getId()) + } else { + ForegroundInfo(id, notification) } } diff --git a/app/src/main/java/com/nextcloud/utils/GlideHelper.kt b/app/src/main/java/com/nextcloud/utils/GlideHelper.kt new file mode 100644 index 0000000..ce89d91 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/GlideHelper.kt @@ -0,0 +1,193 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.graphics.drawable.PictureDrawable +import android.widget.ImageView +import androidx.annotation.DrawableRes +import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory +import androidx.core.net.toUri +import com.bumptech.glide.Glide +import com.bumptech.glide.RequestBuilder +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.load.model.GlideUrl +import com.bumptech.glide.load.model.LazyHeaders +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.BitmapImageViewTarget +import com.bumptech.glide.request.target.Target +import com.nextcloud.common.NextcloudClient +import com.nextcloud.utils.LinkHelper.validateAndGetURL +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.utils.svg.SvgSoftwareLayerSetter + +/** + * Utility object for loading images (including SVGs) using Glide. + * + * Provides methods for loading images into `ImageView`, `Target`, `Target` ... + * from both URLs and URIs. + */ +@Suppress("TooManyFunctions") +object GlideHelper { + private const val TAG = "GlideHelper" + + private class GlideLogger(private val methodName: String, private val identifier: String) : RequestListener { + override fun onLoadFailed(p0: GlideException?, p1: Any?, p2: Target, p3: Boolean): Boolean { + Log_OC.e(TAG, "$methodName: Load failed for $identifier") + Log_OC.e(TAG, "$methodName: Error: ${p0?.message}") + p0?.logRootCauses(TAG) + return false + } + + override fun onResourceReady(p0: T & Any, p1: Any, p2: Target?, p3: DataSource, p4: Boolean): Boolean { + Log_OC.i(TAG, "Glide load completed: $p0") + return false + } + } + + private fun isSVG(url: String): Boolean = (url.toUri().encodedPath?.endsWith(".svg") == true) + + private fun createGlideUrl(url: String, client: NextcloudClient) = GlideUrl( + url, + LazyHeaders.Builder() + .addHeader("Authorization", client.credentials) + .addHeader("User-Agent", "Mozilla/5.0 (Android) Nextcloud-android") + .build() + ) + + private fun RequestBuilder.withLogging(methodName: String, identifier: String): RequestBuilder = + listener(GlideLogger(methodName, identifier)) + + @SuppressLint("CheckResult") + private fun createSvgRequestBuilder( + context: Context, + uri: String, + client: NextcloudClient, + placeholder: Int? = null + ): RequestBuilder { + val glideUrl = createGlideUrl(uri, client) + + return Glide.with(context) + .`as`(PictureDrawable::class.java) + .load(glideUrl) + .apply { + placeholder?.let { placeholder(it) } + placeholder?.let { error(it) } + } + .listener(SvgSoftwareLayerSetter()) + } + + private fun createUrlRequestBuilder( + context: Context, + client: NextcloudClient, + url: String + ): RequestBuilder { + val glideUrl = createGlideUrl(url, client) + return Glide.with(context) + .load(glideUrl) + .centerCrop() + } + + @Suppress("TooGenericExceptionCaught") + fun getBitmap(context: Context, url: String?): Bitmap? { + val validatedUrl = validateAndGetURL(url) ?: return null + + return try { + Glide.with(context) + .asBitmap() + .load(validatedUrl) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + .withLogging("downloadImageSynchronous", validatedUrl) + .submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) + .get() + } catch (e: Exception) { + Log_OC.e(TAG, "Could not download image $e") + null + } + } + + fun loadCircularBitmapIntoImageView(context: Context, url: String?, imageView: ImageView, placeholder: Drawable) { + val validatedUrl = validateAndGetURL(url) ?: return + + Glide.with(context) + .asBitmap() + .load(validatedUrl) + .placeholder(placeholder) + .error(placeholder) + .withLogging("loadCircularBitmapIntoImageView", validatedUrl) + .into(object : BitmapImageViewTarget(imageView) { + override fun setResource(resource: Bitmap?) { + val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(context.resources, resource) + circularBitmapDrawable.isCircular = true + imageView.setImageDrawable(circularBitmapDrawable) + } + }) + } + + @Suppress("UNCHECKED_CAST", "TooGenericExceptionCaught", "ReturnCount") + private fun createRequestBuilder(context: Context, client: NextcloudClient?, url: String?): RequestBuilder? { + if (client == null) { + Log_OC.e(TAG, "Client is null") + return null + } + + val validatedUrl = validateAndGetURL(url) ?: return null + + return try { + val isSVG = isSVG(validatedUrl) + + return if (isSVG) { + createSvgRequestBuilder(context, validatedUrl, client) + } else { + createUrlRequestBuilder(context, client, validatedUrl) + } + .withLogging("createRequestBuilder", validatedUrl) as RequestBuilder? + } catch (e: Exception) { + Log_OC.e(TAG, "Error createRequestBuilder: $e") + null + } + } + + @SuppressLint("CheckResult") + fun loadIntoImageView( + context: Context, + client: NextcloudClient?, + url: String?, + imageView: ImageView, + @DrawableRes placeholder: Int, + circleCrop: Boolean = false + ) { + createRequestBuilder(context, client, url) + ?.placeholder(placeholder) + ?.error(placeholder) + ?.apply { if (circleCrop) circleCrop() } + ?.into(imageView) + } + + fun getDrawable(context: Context, client: NextcloudClient?, urlString: String?): Drawable? = + createRequestBuilder(context, client, urlString)?.submit()?.get() + + fun loadIntoTarget( + context: Context, + client: NextcloudClient?, + url: String, + target: Target, + @DrawableRes placeholder: Int + ) { + createRequestBuilder(context, client, url) + ?.placeholder(placeholder) + ?.error(placeholder) + ?.into(target) + } +} diff --git a/app/src/main/java/com/nextcloud/utils/LinkHelper.kt b/app/src/main/java/com/nextcloud/utils/LinkHelper.kt new file mode 100644 index 0000000..294c98d --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/LinkHelper.kt @@ -0,0 +1,135 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 ZetaTom <70907959+ZetaTom@users.noreply.github.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.core.net.toUri +import com.nextcloud.client.account.User +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.activity.FileDisplayActivity +import java.util.Locale +import java.util.Optional +import kotlin.jvm.optionals.getOrNull + +object LinkHelper { + const val APP_NEXTCLOUD_NOTES = "it.niedermann.owncloud.notes" + const val APP_NEXTCLOUD_TALK = "com.nextcloud.talk2" + private const val TAG = "LinkHelper" + + fun isHttpOrHttpsLink(link: String?): Boolean = link?.lowercase(Locale.getDefault())?.let { + it.startsWith("http://") || it.startsWith("https://") + } == true + + /** + * Open specified app and, if not installed redirect to corresponding download. + * + * @param packageName of app to be opened + * @param user to pass in intent + */ + fun openAppOrStore(packageName: String, user: Optional, context: Context) { + openAppOrStore(packageName, user.getOrNull(), context) + } + + /** + * Open specified app and, if not installed redirect to corresponding download. + * + * @param packageName of app to be opened + * @param user to pass in intent + */ + fun openAppOrStore(packageName: String, user: User?, context: Context) { + val intent = context.packageManager.getLaunchIntentForPackage(packageName) + if (intent != null) { + // app installed - open directly + // TODO handle null user? + intent.putExtra(FileDisplayActivity.KEY_ACCOUNT, user.hashCode()) + context.startActivity(intent) + } else { + // app not found - open market (Google Play Store, F-Droid, etc.) + openAppStore(packageName, false, context) + } + } + + /** + * Open app store page of specified app or search for specified string. Will attempt to open browser when no app + * store is available. + * + * @param string packageName or url-encoded search string + * @param search false -> show app corresponding to packageName; true -> open search for string + */ + fun openAppStore(string: String, search: Boolean = false, context: Context) { + var suffix = (if (search) "search?q=" else "details?id=") + string + val intent = Intent(Intent.ACTION_VIEW, "market://$suffix".toUri()) + try { + context.startActivity(intent) + } catch (activityNotFoundException1: ActivityNotFoundException) { + // all is lost: open google play store web page for app + if (!search) { + suffix = "apps/$suffix" + } + intent.setData("https://play.google.com/store/$suffix".toUri()) + context.startActivity(intent) + } + } + + // region Validation + private const val HTTP = "http" + private const val HTTPS = "https" + private const val FILE = "file" + private const val CONTENT = "content" + + /** + * Validates if a string can be converted to a valid URI + */ + @Suppress("TooGenericExceptionCaught", "ReturnCount") + fun validateAndGetURI(uriString: String?): Uri? { + if (uriString.isNullOrBlank()) { + Log_OC.w(TAG, "Given uriString is null or blank") + return null + } + + return try { + val uri = uriString.toUri() + if (uri.scheme == null) { + return null + } + + val validSchemes = listOf(HTTP, HTTPS, FILE, CONTENT) + if (uri.scheme in validSchemes) uri else null + } catch (e: Exception) { + Log_OC.e(TAG, "Invalid URI string: $uriString -- $e") + null + } + } + + /** + * Validates if a URL string is valid + */ + @Suppress("TooGenericExceptionCaught", "ReturnCount") + fun validateAndGetURL(url: String?): String? { + if (url.isNullOrBlank()) { + Log_OC.w(TAG, "Given url is null or blank") + return null + } + + return try { + val uri = url.toUri() + if (uri.scheme == null) { + return null + } + val validSchemes = listOf(HTTP, HTTPS) + if (uri.scheme in validSchemes) url else null + } catch (e: Exception) { + Log_OC.e(TAG, "Invalid URL: $url -- $e") + null + } + } + // endregion +} diff --git a/app/src/main/java/com/nextcloud/utils/MenuUtils.kt b/app/src/main/java/com/nextcloud/utils/MenuUtils.kt index ce54614..56d150c 100644 --- a/app/src/main/java/com/nextcloud/utils/MenuUtils.kt +++ b/app/src/main/java/com/nextcloud/utils/MenuUtils.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.utils diff --git a/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt b/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt new file mode 100644 index 0000000..429de5a --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt @@ -0,0 +1,56 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.nextcloud.utils + +import androidx.exifinterface.media.ExifInterface +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.utils.BitmapUtils + +object OCFileUtils { + private const val TAG = "OCFileUtils" + + @Suppress("ReturnCount", "NestedBlockDepth") + fun getImageSize(ocFile: OCFile, defaultThumbnailSize: Float): Pair { + try { + Log_OC.d(TAG, "Getting image size for: ${ocFile.fileName}") + + if (!ocFile.exists()) { + ocFile.imageDimension?.width?.let { w -> + ocFile.imageDimension?.height?.let { h -> + return w.toInt() to h.toInt() + } + } + val size = defaultThumbnailSize.toInt().coerceAtLeast(1) + return size to size + } + + val exif = ExifInterface(ocFile.storagePath) + val width = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0) + val height = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0) + + if (width > 0 && height > 0) { + Log_OC.d(TAG, "Exif used width: $width and height: $height") + return width to height + } + + val (bitmapWidth, bitmapHeight) = BitmapUtils.getImageResolution(ocFile.storagePath) + .let { it[0] to it[1] } + + if (bitmapWidth > 0 && bitmapHeight > 0) { + Log_OC.d(TAG, "BitmapUtils.getImageResolution used width: $bitmapWidth and height: $bitmapHeight") + return bitmapWidth to bitmapHeight + } + + val fallback = defaultThumbnailSize.toInt().coerceAtLeast(1) + Log_OC.d(TAG, "Default size used width: $fallback and height: $fallback") + return fallback to fallback + } finally { + Log_OC.d(TAG, "-----------------------------") + } + } +} diff --git a/app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt b/app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt index 16b8bc3..e2ad9e5 100644 --- a/app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt +++ b/app/src/main/java/com/nextcloud/utils/ShortcutUtil.kt @@ -1,10 +1,10 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Felix Nüsse - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.utils @@ -14,12 +14,13 @@ import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.graphics.Canvas -import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.createBitmap import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.toBitmap +import androidx.core.graphics.drawable.toDrawable import com.nextcloud.client.account.User import com.owncloud.android.R import com.owncloud.android.datamodel.OCFile @@ -44,52 +45,62 @@ class ShortcutUtil @Inject constructor(private val mContext: Context) { user: User, syncedFolderProvider: SyncedFolderProvider ) { - if (ShortcutManagerCompat.isRequestPinShortcutSupported(mContext)) { - val intent = Intent(mContext, FileDisplayActivity::class.java) - intent.action = FileDisplayActivity.OPEN_FILE - intent.putExtra(FileActivity.EXTRA_FILE, file.remotePath) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - val shortcutId = "nextcloud_shortcut_" + file.remoteId - val icon: IconCompat - var thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( - ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.remoteId - ) - if (thumbnail != null) { - thumbnail = bitmapToAdaptiveBitmap(thumbnail) - icon = IconCompat.createWithAdaptiveBitmap(thumbnail) - } else if (file.isFolder) { + if (!ShortcutManagerCompat.isRequestPinShortcutSupported(mContext)) { + return + } + + val intent = Intent(mContext, FileDisplayActivity::class.java).apply { + action = FileDisplayActivity.OPEN_FILE + putExtra(FileActivity.EXTRA_FILE_REMOTE_PATH, file.decryptedRemotePath) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + } + + val icon = createShortcutIcon(file, viewThemeUtils, user, syncedFolderProvider) + + val shortcutInfo = ShortcutInfoCompat.Builder(mContext, "nextcloud_shortcut_${file.remoteId}") + .setShortLabel(file.fileName) + .setLongLabel(mContext.getString(R.string.pin_shortcut_label, file.fileName)) + .setIcon(icon) + .setIntent(intent) + .build() + + val resultIntent = + ShortcutManagerCompat.createShortcutResultIntent(mContext, shortcutInfo) + + val pendingIntent = PendingIntent.getBroadcast( + mContext, + file.hashCode(), + resultIntent, + FLAG_IMMUTABLE + ) + + ShortcutManagerCompat.requestPinShortcut(mContext, shortcutInfo, pendingIntent.intentSender) + } + + private fun createShortcutIcon( + file: OCFile, + viewThemeUtils: ViewThemeUtils, + user: User, + syncedFolderProvider: SyncedFolderProvider + ): IconCompat { + val thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( + ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.remoteId + ) + + return when { + thumbnail != null -> IconCompat.createWithAdaptiveBitmap(bitmapToAdaptiveBitmap(thumbnail)) + + file.isFolder -> { val isAutoUploadFolder = SyncedFolderProvider.isAutoUploadFolder(syncedFolderProvider, file, user) val isDarkModeActive = syncedFolderProvider.preferences.isDarkModeEnabled - val overlayIconId = file.getFileOverlayIconId(isAutoUploadFolder) - val drawable = MimeTypeUtil.getFileIcon(isDarkModeActive, overlayIconId, mContext, viewThemeUtils) - val bitmapIcon = drawable.toBitmap() - icon = IconCompat.createWithBitmap(bitmapIcon) - } else { - icon = IconCompat.createWithResource( - mContext, - MimeTypeUtil.getFileTypeIconId(file.mimeType, file.fileName) - ) + val drawable = MimeTypeUtil.getFolderIcon(isDarkModeActive, overlayIconId, mContext, viewThemeUtils) + IconCompat.createWithBitmap(drawable.toBitmap()) } - val longLabel = mContext.getString(R.string.pin_shortcut_label, file.fileName) - val pinShortcutInfo = ShortcutInfoCompat.Builder(mContext, shortcutId) - .setShortLabel(file.fileName) - .setLongLabel(longLabel) - .setIcon(icon) - .setIntent(intent) - .build() - val pinnedShortcutCallbackIntent = - ShortcutManagerCompat.createShortcutResultIntent(mContext, pinShortcutInfo) - val successCallback = PendingIntent.getBroadcast( + + else -> IconCompat.createWithResource( mContext, - 0, - pinnedShortcutCallbackIntent, - FLAG_IMMUTABLE - ) - ShortcutManagerCompat.requestPinShortcut( - mContext, - pinShortcutInfo, - successCallback.intentSender + MimeTypeUtil.getFileTypeIconId(file.mimeType, file.fileName) ) } } @@ -97,8 +108,8 @@ class ShortcutUtil @Inject constructor(private val mContext: Context) { private fun bitmapToAdaptiveBitmap(orig: Bitmap): Bitmap { val adaptiveIconSize = mContext.resources.getDimensionPixelSize(R.dimen.adaptive_icon_size) val adaptiveIconOuterSides = mContext.resources.getDimensionPixelSize(R.dimen.adaptive_icon_padding) - val drawable: Drawable = BitmapDrawable(mContext.resources, orig) - val bitmap = Bitmap.createBitmap(adaptiveIconSize, adaptiveIconSize, Bitmap.Config.ARGB_8888) + val drawable: Drawable = orig.toDrawable(mContext.resources) + val bitmap = createBitmap(adaptiveIconSize, adaptiveIconSize) val canvas = Canvas(bitmap) drawable.setBounds( adaptiveIconOuterSides, diff --git a/app/src/main/java/com/nextcloud/utils/TimeConstants.kt b/app/src/main/java/com/nextcloud/utils/TimeConstants.kt index 7663059..7c57976 100644 --- a/app/src/main/java/com/nextcloud/utils/TimeConstants.kt +++ b/app/src/main/java/com/nextcloud/utils/TimeConstants.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.utils diff --git a/app/src/main/java/com/nextcloud/utils/autoRename/AutoRename.kt b/app/src/main/java/com/nextcloud/utils/autoRename/AutoRename.kt new file mode 100644 index 0000000..11dba9a --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/autoRename/AutoRename.kt @@ -0,0 +1,135 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.autoRename + +import com.nextcloud.utils.extensions.StringConstants +import com.nextcloud.utils.extensions.checkWCFRestrictions +import com.nextcloud.utils.extensions.forbiddenFilenameCharacters +import com.nextcloud.utils.extensions.forbiddenFilenameExtensions +import com.nextcloud.utils.extensions.shouldRemoveNonPrintableUnicodeCharactersAndConvertToUTF8 +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.status.OCCapability +import org.apache.commons.io.FilenameUtils +import java.util.regex.Pattern + +object AutoRename { + private const val TAG = "AutoRename" + private const val REPLACEMENT = "_" + + @Suppress("NestedBlockDepth") + @JvmOverloads + fun rename(filename: String, capability: OCCapability, isFolderPath: Boolean? = null): String { + if (!capability.checkWCFRestrictions()) { + return filename + } + + Log_OC.d(TAG, "Before - $filename") + + val isFolder = isFolderPath ?: filename.endsWith(OCFile.PATH_SEPARATOR) + val pathSegments = filename.split(OCFile.PATH_SEPARATOR).toMutableList() + + capability.run { + if (forbiddenFilenameCharactersJson != null) { + var forbiddenFilenameCharacters = capability.forbiddenFilenameCharacters() + + if (isFolder) { + forbiddenFilenameCharacters = forbiddenFilenameCharacters.filter { it != OCFile.PATH_SEPARATOR } + } + + pathSegments.replaceAll { segment -> + var modifiedSegment = segment + + forbiddenFilenameCharacters.forEach { forbiddenChar -> + if (modifiedSegment.contains(forbiddenChar)) { + modifiedSegment = modifiedSegment.replace(forbiddenChar, REPLACEMENT) + } + } + + modifiedSegment + } + } + + if (forbiddenFilenameExtensionJson != null) { + val forbiddenFilenameExtensions = forbiddenFilenameExtensions() + + forbiddenFilenameExtensions.find { it == StringConstants.SPACE }?.let { + pathSegments.replaceAll { segment -> + segment.trim() + } + } + + forbiddenFilenameExtensions.find { it == StringConstants.DOT }?.let { forbiddenExtension -> + pathSegments.replaceAll { segment -> + replaceDots(forbiddenExtension, segment) + } + } + + forbiddenFilenameExtensions + .filter { it != StringConstants.SPACE && it != StringConstants.DOT } + .forEach { forbiddenExtension -> + pathSegments.replaceAll { segment -> + replaceFileExtensions(forbiddenExtension, segment) + } + } + } + } + + val filenameWithExtension = pathSegments.joinToString(OCFile.PATH_SEPARATOR) + val updatedFileName = if (isFolder) filenameWithExtension else lowercaseFileExtension(filenameWithExtension) + + val result = if (capability.shouldRemoveNonPrintableUnicodeCharactersAndConvertToUTF8()) { + val utf8Result = convertToUTF8(updatedFileName) + removeNonPrintableUnicodeCharacters(utf8Result) + } else { + updatedFileName + }.trim() + + Log_OC.d(TAG, "After - $result") + + return result + } + + private fun lowercaseFileExtension(filename: String): String { + val extension = FilenameUtils.getExtension(filename).lowercase() + val filenameWithoutExtension = FilenameUtils.removeExtension(filename) + return if (extension.isNotEmpty()) { + filenameWithoutExtension + StringConstants.DOT + extension + } else { + filenameWithoutExtension + } + } + + private fun replaceDots(forbiddenExtension: String, segment: String): String = + if (isSegmentContainsForbiddenExtension(forbiddenExtension, segment)) { + segment.replaceFirst(forbiddenExtension, REPLACEMENT) + } else { + segment + } + + private fun replaceFileExtensions(forbiddenExtension: String, segment: String): String = + if (isSegmentContainsForbiddenExtension(forbiddenExtension, segment)) { + val newExtension = forbiddenExtension.replace(StringConstants.DOT, REPLACEMENT, ignoreCase = true) + segment.replace(forbiddenExtension, newExtension.lowercase(), ignoreCase = true) + } else { + segment + } + + private fun isSegmentContainsForbiddenExtension(forbiddenExtension: String, segment: String): Boolean = + segment.endsWith(forbiddenExtension, ignoreCase = true) || + segment.startsWith(forbiddenExtension, ignoreCase = true) + + private fun convertToUTF8(filename: String): String = String(filename.toByteArray(), Charsets.UTF_8) + + private fun removeNonPrintableUnicodeCharacters(filename: String): String { + val regex = "\\p{C}" + val pattern = Pattern.compile(regex) + val matcher = pattern.matcher(filename) + return matcher.replaceAll("") + } +} diff --git a/app/src/main/java/com/nextcloud/utils/date/DateFormatPattern.kt b/app/src/main/java/com/nextcloud/utils/date/DateFormatPattern.kt new file mode 100644 index 0000000..a3852d6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/date/DateFormatPattern.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.date + +enum class DateFormatPattern(val pattern: String) { + /** + * 10.11.2024 - 12:44 + */ + FullDateWithHours("dd.MM.yyyy - HH:mm"), + + /** + * Aug 3 + */ + MonthWithDate("MMM d") +} diff --git a/app/src/main/java/com/nextcloud/utils/date/DateFormatter.kt b/app/src/main/java/com/nextcloud/utils/date/DateFormatter.kt new file mode 100644 index 0000000..ea43a5a --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/date/DateFormatter.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.date + +import android.icu.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +object DateFormatter { + + /** + * Converts a Unix timestamp (in milliseconds) into a formatted date string. + * For example, input 1733309160885 with "MMM d" pattern outputs "Dec 4". + */ + @Suppress("MagicNumber") + fun timestampToDateRepresentation(timestamp: Long, formatPattern: DateFormatPattern): String { + val date = Date(timestamp * 1000) + val format = SimpleDateFormat(formatPattern.pattern, Locale.getDefault()) + return format.format(date) + } +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/AccountExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/AccountExtensions.kt index c17f93c..2f70ada 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/AccountExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/AccountExtensions.kt @@ -1,7 +1,7 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Your Name + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ diff --git a/app/src/main/java/com/nextcloud/utils/extensions/ActionBarExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/ActionBarExtensions.kt new file mode 100644 index 0000000..3bafc9b --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/ActionBarExtensions.kt @@ -0,0 +1,19 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import android.text.Spannable +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import androidx.appcompat.app.ActionBar + +fun ActionBar.setTitleColor(color: Int) { + val text = SpannableString(title ?: "") + text.setSpan(ForegroundColorSpan(color), 0, text.length, Spannable.SPAN_INCLUSIVE_INCLUSIVE) + title = text +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/ActivityExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/ActivityExtensions.kt index c60390f..19c9686 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/ActivityExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/ActivityExtensions.kt @@ -7,9 +7,25 @@ package com.nextcloud.utils.extensions +import android.app.Activity +import android.content.Intent import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment -fun AppCompatActivity.isDialogFragmentReady(fragment: Fragment): Boolean = isActive() && !fragment.isStateSaved() +fun AppCompatActivity.isDialogFragmentReady(fragment: Fragment): Boolean = isActive() && !fragment.isStateSaved fun AppCompatActivity.isActive(): Boolean = !isFinishing && !isDestroyed + +fun AppCompatActivity.fragments(): List = supportFragmentManager.fragments + +fun AppCompatActivity.lastFragment(): Fragment? = supportFragmentManager.fragments.lastOrNull { it.isVisible } + +fun Activity.showShareIntent(text: String?) { + val sendIntent = Intent(Intent.ACTION_SEND).apply { + putExtra(Intent.EXTRA_TEXT, text) + type = "text/plain" + } + + val shareIntent = Intent.createChooser(sendIntent, null) + startActivity(shareIntent) +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/BundleExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/BundleExtensions.kt index aa0cccc..d6ddbf4 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/BundleExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/BundleExtensions.kt @@ -1,41 +1,23 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.utils.extensions -import android.os.Build import android.os.Bundle import android.os.Parcelable -import com.owncloud.android.lib.common.utils.Log_OC +import androidx.core.os.BundleCompat import java.io.Serializable -@Suppress("TopLevelPropertyNaming") -private const val tag = "BundleExtension" - fun Bundle?.getSerializableArgument(key: String, type: Class): T? { if (this == null) { return null } - return try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - this.getSerializable(key, type) - } else { - @Suppress("UNCHECKED_CAST", "DEPRECATION") - if (type.isInstance(this.getSerializable(key))) { - this.getSerializable(key) as T - } else { - null - } - } - } catch (e: ClassCastException) { - Log_OC.e(tag, e.localizedMessage) - null - } + return BundleCompat.getSerializable(this, key, type) } fun Bundle?.getParcelableArgument(key: String, type: Class): T? { @@ -43,16 +25,5 @@ fun Bundle?.getParcelableArgument(key: String, type: Class) return null } - return try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - this.getParcelable(key, type) - } else { - @Suppress("DEPRECATION") - this.getParcelable(key) - } - } catch (e: ClassCastException) { - Log_OC.e(tag, e.localizedMessage) - e.printStackTrace() - null - } + return BundleCompat.getParcelable(this, key, type) } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/ContextExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/ContextExtensions.kt index e1ff053..6a2441b 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/ContextExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/ContextExtensions.kt @@ -1,25 +1,71 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.utils.extensions import android.annotation.SuppressLint +import android.app.Activity import android.content.BroadcastReceiver import android.content.Context +import android.content.ContextWrapper import android.content.Intent import android.content.IntentFilter import android.os.Build +import android.os.Handler +import android.os.Looper +import android.view.WindowInsets +import android.view.WindowManager +import android.widget.Toast +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import com.owncloud.android.R import com.owncloud.android.datamodel.ReceiverFlag +fun Context.hourPlural(hour: Int): String = resources.getQuantityString(R.plurals.hours, hour, hour) + +fun Context.minPlural(min: Int): String = resources.getQuantityString(R.plurals.minutes, min, min) + @SuppressLint("UnspecifiedRegisterReceiverFlag") -fun Context.registerBroadcastReceiver(receiver: BroadcastReceiver?, filter: IntentFilter, flag: ReceiverFlag): Intent? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { +fun Context.registerBroadcastReceiver(receiver: BroadcastReceiver?, filter: IntentFilter, flag: ReceiverFlag): Intent? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { registerReceiver(receiver, filter, flag.getId()) } else { registerReceiver(receiver, filter) } + +fun Context.statusBarHeight(): Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val windowInsets = (getSystemService(Context.WINDOW_SERVICE) as WindowManager) + .currentWindowMetrics + .windowInsets + val insets = windowInsets.getInsets(WindowInsets.Type.statusBars()) + insets.top +} else { + @Suppress("DEPRECATION") + val decorView = (getSystemService(Context.WINDOW_SERVICE) as WindowManager) + .defaultDisplay + .let { display -> + val decorView = android.view.View(this) + display.getRealMetrics(android.util.DisplayMetrics()) + decorView + } + val windowInsetsCompat = ViewCompat.getRootWindowInsets(decorView) + windowInsetsCompat?.getInsets(WindowInsetsCompat.Type.statusBars())?.top ?: 0 +} + +fun Context.showToast(message: String) { + Handler(Looper.getMainLooper()).post { + Toast.makeText(this, message, Toast.LENGTH_LONG).show() + } +} + +fun Context.showToast(messageId: Int) = showToast(getString(messageId)) + +fun Context.getActivity(): Activity? = when (this) { + is Activity -> this + is ContextWrapper -> baseContext.getActivity() + else -> null } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/DateExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/DateExtensions.kt new file mode 100644 index 0000000..cda0532 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/DateExtensions.kt @@ -0,0 +1,17 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import android.annotation.SuppressLint +import com.nextcloud.utils.date.DateFormatPattern +import java.text.SimpleDateFormat +import java.util.Date + +@SuppressLint("SimpleDateFormat") +fun Date.currentDateRepresentation(formatPattern: DateFormatPattern): String = + SimpleDateFormat(formatPattern.pattern).format(this) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/DecryptedUserExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/DecryptedUserExtensions.kt new file mode 100644 index 0000000..ba48306 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/DecryptedUserExtensions.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedUser + +fun List.findMetadataKeyByUserId(userId: String): String? { + var result: String? = null + + for (decryptedUser in this) { + if (decryptedUser != null && decryptedUser.userId == userId) { + result = decryptedUser.decryptedMetadataKey + } + } + + return result +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/DrawableExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/DrawableExtensions.kt new file mode 100644 index 0000000..3fcefbb --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/DrawableExtensions.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.PictureDrawable +import androidx.core.graphics.createBitmap + +fun PictureDrawable.toBitmap(): Bitmap { + val bitmap = createBitmap(picture.getWidth(), picture.getHeight()) + val canvas = Canvas(bitmap) + picture.draw(canvas) + return bitmap +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/DrawerActivityExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/DrawerActivityExtensions.kt new file mode 100644 index 0000000..899d9ed --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/DrawerActivityExtensions.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import android.content.Intent +import com.owncloud.android.MainApp +import com.owncloud.android.R +import com.owncloud.android.ui.activity.DrawerActivity +import com.owncloud.android.ui.activity.FileDisplayActivity + +fun DrawerActivity.navigateToAllFiles() { + DrawerActivity.menuItemId = R.id.nav_all_files + setNavigationViewItemChecked() + + MainApp.showOnlyFilesOnDevice(false) + MainApp.showOnlyPersonalFiles(false) + + Intent(applicationContext, FileDisplayActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + action = FileDisplayActivity.ALL_FILES + }.run { + startActivity(this) + } +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/Extensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/Extensions.kt index b9ff09c..598bff9 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/Extensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/Extensions.kt @@ -1,12 +1,15 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2024 TSI-mc * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.utils.extensions +import android.os.Handler +import android.os.Looper import android.os.SystemClock import android.text.Selection import android.text.Spannable @@ -17,10 +20,18 @@ import android.text.method.LinkMovementMethod import android.text.style.ClickableSpan import android.view.View import android.widget.TextView +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.resources.trashbin.model.TrashbinFile import java.text.SimpleDateFormat import java.util.Date import java.util.Locale +fun mainThread(delay: Long = 1000, action: () -> Unit) { + Handler(Looper.getMainLooper()).postDelayed({ + action() + }, delay) +} + fun clickWithDebounce(view: View, debounceTime: Long = 600L, action: () -> Unit) { view.setOnClickListener(object : View.OnClickListener { private var lastClickTime: Long = 0 @@ -79,3 +90,12 @@ fun Long.getFormattedStringDate(format: String): String { val simpleDateFormat = SimpleDateFormat(format, Locale.getDefault()) return simpleDateFormat.format(Date(this)) } + +fun TrashbinFile.toOCFile(): OCFile { + val ocFile = OCFile(this.remotePath) + ocFile.mimeType = this.mimeType + ocFile.fileLength = this.fileLength + ocFile.remoteId = this.remoteId + ocFile.fileName = this.fileName + return ocFile +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt new file mode 100644 index 0000000..2fec3fc --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt @@ -0,0 +1,42 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile + +fun FileDataStorageManager.searchFilesByName(file: OCFile, accountName: String, query: String): List = + fileDao.searchFilesInFolder(file.fileId, accountName, query).map { + createFileInstance(it) + } + +fun FileDataStorageManager.getDecryptedPath(file: OCFile): String { + val paths = mutableListOf() + var entity = fileDao.getFileByEncryptedRemotePath(file.remotePath, user.accountName) + + while (entity != null) { + entity.name?.takeIf { it.isNotEmpty() }?.let { + paths.add(it.removePrefix(OCFile.PATH_SEPARATOR)) + } + entity = entity.parent?.let { fileDao.getFileById(it) } ?: break + } + + return paths + .reversed() + .joinToString(OCFile.PATH_SEPARATOR) +} + +fun FileDataStorageManager.getSubfiles(id: Long, accountName: String): List = + fileDao.getSubfiles(id, accountName).map { + createFileInstance(it) + } + +fun FileDataStorageManager.getNonEncryptedSubfolders(id: Long, accountName: String): List = + fileDao.getNonEncryptedSubfolders(id, accountName).map { + createFileInstance(it) + } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FileExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FileExtensions.kt new file mode 100644 index 0000000..a55345d --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/FileExtensions.kt @@ -0,0 +1,52 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.utils.DisplayUtils +import java.io.File +import java.nio.file.Path + +private const val TAG = "FileExtensions" + +fun OCFile?.logFileSize(tag: String) { + val size = DisplayUtils.bytesToHumanReadable(this?.fileLength ?: -1) + val rawByte = this?.fileLength ?: -1 + Log_OC.d(tag, "onSaveInstanceState: $size, raw byte $rawByte") +} + +fun File?.logFileSize(tag: String) { + val size = DisplayUtils.bytesToHumanReadable(this?.length() ?: -1) + val rawByte = this?.length() ?: -1 + Log_OC.d(tag, "onSaveInstanceState: $size, raw byte $rawByte") +} + +fun Path.toLocalPath(): String = toAbsolutePath().toString() + +/** + * Converts a non-null and non-empty [String] path into a [File] object, if it exists. + * + * @receiver String path to a file. + * @return [File] instance if the file exists, or `null` if the path is null, empty, or non-existent. + */ +@Suppress("ReturnCount") +fun String.toFile(): File? { + if (isNullOrEmpty()) { + Log_OC.e(TAG, "given path is null or empty") + return null + } + + val file = File(this) + if (!file.exists()) { + Log_OC.e(TAG, "File does not exist: $this") + return null + } + + return file +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FragmentExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FragmentExtensions.kt new file mode 100644 index 0000000..fafa7fd --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/FragmentExtensions.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle + +inline fun Fragment.typedActivity(): T? = if (isAdded && activity != null && activity is T) { + activity as T +} else { + null +} + +/** + * Extension for Java Classes + */ +fun Fragment.getTypedActivity(type: Class): T? = + if (isAdded && activity != null && type.isInstance(activity)) { + type.cast(activity) + } else { + null + } + +fun Fragment.isDialogFragmentReady() = + isAdded && !isStateSaved && activity?.lifecycle?.currentState?.isAtLeast(Lifecycle.State.RESUMED) == true diff --git a/app/src/main/java/com/nextcloud/utils/extensions/ImageViewExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/ImageViewExtensions.kt new file mode 100644 index 0000000..896d26d --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/ImageViewExtensions.kt @@ -0,0 +1,49 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import android.content.Context +import android.graphics.drawable.GradientDrawable +import android.util.TypedValue +import android.view.ViewOutlineProvider +import android.widget.ImageView +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat +import com.owncloud.android.R + +@JvmOverloads +fun ImageView.makeRoundedWithIcon( + context: Context, + @DrawableRes icon: Int, + paddingDp: Int = 6, + @ColorInt backgroundColor: Int = ContextCompat.getColor(context, R.color.primary), + @ColorInt foregroundColor: Int = ContextCompat.getColor(context, R.color.white) +) { + setImageResource(icon) + + val drawable = GradientDrawable().apply { + shape = GradientDrawable.OVAL + setColor(backgroundColor) + } + + background = drawable + clipToOutline = true + scaleType = ImageView.ScaleType.CENTER_INSIDE + outlineProvider = ViewOutlineProvider.BACKGROUND + + setColorFilter(foregroundColor) + + val paddingPx = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + paddingDp.toFloat(), + context.resources.displayMetrics + ).toInt() + + setPadding(paddingPx, paddingPx, paddingPx, paddingPx) +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/IntExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/IntExtensions.kt new file mode 100644 index 0000000..df10cbc --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/IntExtensions.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import java.nio.ByteBuffer + +@Suppress("MagicNumber") +fun IntArray.toByteArray(): ByteArray { + val byteBuffer = ByteBuffer.allocate(this.size * 4) + val intBuffer = byteBuffer.asIntBuffer() + intBuffer.put(this) + return byteBuffer.array() +} + +@Suppress("MagicNumber") +fun ByteArray.toIntArray(): IntArray { + val intBuffer = ByteBuffer.wrap(this).asIntBuffer() + val intArray = IntArray(this.size / 4) + intBuffer.get(intArray) + return intArray +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/IntentExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/IntentExtensions.kt index d709068..56da87f 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/IntentExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/IntentExtensions.kt @@ -1,41 +1,23 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.utils.extensions import android.content.Intent -import android.os.Build import android.os.Parcelable -import com.owncloud.android.lib.common.utils.Log_OC +import androidx.core.content.IntentCompat import java.io.Serializable -@Suppress("TopLevelPropertyNaming") -private const val tag = "IntentExtension" - fun Intent?.getSerializableArgument(key: String, type: Class): T? { if (this == null) { return null } - return try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - this.getSerializableExtra(key, type) - } else { - @Suppress("UNCHECKED_CAST", "DEPRECATION") - if (type.isInstance(this.getSerializableExtra(key))) { - this.getSerializableExtra(key) as T - } else { - null - } - } - } catch (e: ClassCastException) { - Log_OC.e(tag, e.localizedMessage) - null - } + return IntentCompat.getSerializableExtra(this, key, type) } fun Intent?.getParcelableArgument(key: String, type: Class): T? { @@ -43,15 +25,5 @@ fun Intent?.getParcelableArgument(key: String, type: Class) return null } - return try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - this.getParcelableExtra(key, type) - } else { - @Suppress("DEPRECATION") - this.getParcelableExtra(key) - } - } catch (e: ClassCastException) { - Log_OC.e(tag, e.localizedMessage) - null - } + return IntentCompat.getParcelableExtra(this, key, type) } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OCCapabilityExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OCCapabilityExtensions.kt new file mode 100644 index 0000000..8628548 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/OCCapabilityExtensions.kt @@ -0,0 +1,59 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.google.gson.Gson +import com.owncloud.android.lib.resources.status.NextcloudVersion +import com.owncloud.android.lib.resources.status.OCCapability +import org.json.JSONException + +private val gson = Gson() + +/** + * Determines whether **Windows-compatible file (WCF)** restrictions should be applied + * for the current server version and configuration. + * + * Behavior: + * - For **Nextcloud 32 and newer**, WCF enforcement depends on the [`isWCFEnabled`] flag + * provided by the server capabilities. + * - For **Nextcloud 30 and 31**, WCF restrictions are always applied (feature considered enabled). + * - For **versions older than 30**, WCF is not supported, and no restrictions are applied. + * + * @return `true` if WCF restrictions should be enforced based on the server version and configuration; + * `false` otherwise. + */ +fun OCCapability.checkWCFRestrictions(): Boolean = if (version.isNewerOrEqual(NextcloudVersion.nextcloud_32)) { + isWCFEnabled.isTrue +} else { + version.isNewerOrEqual(NextcloudVersion.nextcloud_30) +} + +fun OCCapability.forbiddenFilenames(): List = jsonToList(forbiddenFilenamesJson) + +fun OCCapability.forbiddenFilenameCharacters(): List = jsonToList(forbiddenFilenameCharactersJson) + +fun OCCapability.forbiddenFilenameExtensions(): List = jsonToList(forbiddenFilenameExtensionJson) + +fun OCCapability.forbiddenFilenameBaseNames(): List = jsonToList(forbiddenFilenameBaseNamesJson) + +fun OCCapability.shouldRemoveNonPrintableUnicodeCharactersAndConvertToUTF8(): Boolean = + forbiddenFilenames().isNotEmpty() || + forbiddenFilenameCharacters().isNotEmpty() || + forbiddenFilenameExtensions().isNotEmpty() || + forbiddenFilenameBaseNames().isNotEmpty() + +@Suppress("ReturnCount") +private fun jsonToList(json: String?): List { + if (json == null) return emptyList() + + return try { + return gson.fromJson(json, Array::class.java).toList() + } catch (_: JSONException) { + emptyList() + } +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt new file mode 100644 index 0000000..1e32739 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt @@ -0,0 +1,33 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.owncloud.android.MainApp +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.utils.FileStorageUtils + +fun List.filterFilenames(): List = distinctBy { it.fileName } + +fun List.filterTempFilter(): List = filterNot { it.isTempFile() } + +fun OCFile.isTempFile(): Boolean { + val context = MainApp.getAppContext() + val appTempPath = FileStorageUtils.getAppTempDirectoryPath(context) + return storagePath?.startsWith(appTempPath) == true +} + +fun List.filterHiddenFiles(): List = filterNot { it.isHidden }.distinct() + +fun List.filterByMimeType(mimeType: String): List = + filter { it.isFolder || it.mimeType.startsWith(mimeType) } + +fun List.limitToPersonalFiles(userId: String): List = filter { file -> + file.ownerId?.let { ownerId -> + ownerId == userId && !file.isSharedWithMe && !file.mounted() + } == true +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OCShareExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OCShareExtensions.kt new file mode 100644 index 0000000..9161bf9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/OCShareExtensions.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.owncloud.android.lib.resources.shares.OCShare + +fun OCShare.hasFileRequestPermission(): Boolean = (isFolder && shareType?.isPublicOrMail() == true) + +fun List.mergeDistinctByToken(other: List): List = (this + other).distinctBy { it.token } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OCUploadExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OCUploadExtensions.kt new file mode 100644 index 0000000..6b2550d --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/OCUploadExtensions.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.owncloud.android.db.OCUpload + +fun List.getUploadIds(): LongArray = map { it.uploadId }.toLongArray() + +fun Array.getUploadIds(): LongArray = map { it.uploadId }.toLongArray() diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OnDataTransferProgressListenerExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OnDataTransferProgressListenerExtensions.kt new file mode 100644 index 0000000..f6fed6f --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/OnDataTransferProgressListenerExtensions.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.owncloud.android.lib.common.network.OnDatatransferProgressListener + +@Suppress("MagicNumber") +fun OnDatatransferProgressListener.getPercent(totalTransferredSoFar: Long, totalToTransfer: Long): Int = + ((100.0 * totalTransferredSoFar.toDouble() / totalToTransfer.toDouble()).toInt()).coerceAtMost(100) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OwnCloudClientExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OwnCloudClientExtensions.kt new file mode 100644 index 0000000..ee36c24 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/OwnCloudClientExtensions.kt @@ -0,0 +1,22 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2023 ZetaTom + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ + +package com.nextcloud.utils.extensions + +import android.content.Context +import com.nextcloud.common.NextcloudClient +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.OwnCloudClientFactory + +fun OwnCloudClient.toNextcloudClient(context: Context): NextcloudClient = OwnCloudClientFactory.createNextcloudClient( + baseUri, + userId, + credentials.toOkHttpCredentials(), + context, + isFollowRedirects +) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/ParcableExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/ParcableExtensions.kt new file mode 100644 index 0000000..86e1a26 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/ParcableExtensions.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import android.os.Parcel +import android.os.Parcelable +import androidx.core.os.ParcelCompat + +inline fun Parcel?.readParcelableCompat(classLoader: ClassLoader?): T? { + if (this == null) { + return null + } + + return ParcelCompat.readParcelable(this, classLoader, T::class.java) +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/RemoteOperationResultExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/RemoteOperationResultExtensions.kt new file mode 100644 index 0000000..caf0ad8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/RemoteOperationResultExtensions.kt @@ -0,0 +1,64 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.owncloud.android.MainApp +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode +import com.owncloud.android.lib.resources.files.model.RemoteFile +import com.owncloud.android.utils.ErrorMessageAdapter +import com.owncloud.android.utils.FileStorageUtils + +@Suppress("ReturnCount") +fun Pair?, RemoteOperation<*>?>?.getErrorMessage(): String { + val result = this?.first ?: return MainApp.string(R.string.unexpected_error_occurred) + val operation = this.second ?: return MainApp.string(R.string.unexpected_error_occurred) + return ErrorMessageAdapter.getErrorCauseMessage(result, operation, MainApp.getAppContext().resources) +} + +fun ResultCode.isFileSpecificError(): Boolean { + val errorCodes = listOf( + ResultCode.INSTANCE_NOT_CONFIGURED, + ResultCode.QUOTA_EXCEEDED, + ResultCode.LOCAL_STORAGE_FULL, + ResultCode.WRONG_CONNECTION, + ResultCode.UNAUTHORIZED, + ResultCode.OK_NO_SSL, + ResultCode.MAINTENANCE_MODE, + ResultCode.UNTRUSTED_DOMAIN, + ResultCode.ACCOUNT_NOT_THE_SAME, + ResultCode.ACCOUNT_EXCEPTION, + ResultCode.ACCOUNT_NOT_NEW, + ResultCode.ACCOUNT_NOT_FOUND, + ResultCode.ACCOUNT_USES_STANDARD_PASSWORD, + ResultCode.INCORRECT_ADDRESS, + ResultCode.BAD_OC_VERSION + ) + + return !errorCodes.contains(this) +} + +@Suppress("Deprecation") +fun RemoteOperationResult<*>?.toOCFile(): List? = if (this?.isSuccess == true) { + data?.toOCFileList() +} else { + null +} + +private fun ArrayList.toOCFileList(): List = this.mapNotNull { + val remoteFile = (it as? RemoteFile) + + remoteFile?.let { + remoteFile.toOCFile() + } +} + +private fun RemoteFile?.toOCFile(): OCFile = FileStorageUtils.fillOCFile(this) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/SearchResultEntryExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/SearchResultEntryExtensions.kt new file mode 100644 index 0000000..6db53ba --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/SearchResultEntryExtensions.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.nextcloud.model.SearchResultEntryType +import com.owncloud.android.lib.common.SearchResultEntry + +fun SearchResultEntry.getType(): SearchResultEntryType = if (icon == "icon-folder") { + SearchResultEntryType.Folder +} else if (icon.startsWith("icon-note")) { + SearchResultEntryType.Note +} else if (icon.startsWith("icon-contacts")) { + SearchResultEntryType.Contact +} else if (icon.startsWith("icon-calendar")) { + SearchResultEntryType.CalendarEvent +} else if (icon.startsWith("icon-deck")) { + SearchResultEntryType.Deck +} else { + SearchResultEntryType.Unknown +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/ShareTypeExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/ShareTypeExtensions.kt new file mode 100644 index 0000000..4200fae --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/ShareTypeExtensions.kt @@ -0,0 +1,12 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.owncloud.android.lib.resources.shares.ShareType + +fun ShareType.isPublicOrMail(): Boolean = (this == ShareType.PUBLIC_LINK || this == ShareType.EMAIL) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/StringExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/StringExtensions.kt index 9888e95..b802bac 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/StringExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/StringExtensions.kt @@ -1,8 +1,8 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Your Name - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.utils.extensions @@ -15,3 +15,40 @@ fun String.getRandomString(length: Int): String { return this + result } + +fun String.removeFileExtension(): String { + val dotIndex = lastIndexOf('.') + return if (dotIndex != -1) { + substring(0, dotIndex) + } else { + this + } +} + +/** + * Checks if two nullable strings are both valid (non-null, non-empty, non-blank) and equal. + * + * It returns `true` only when both strings meet all the following criteria: + * - Neither string is null + * - Neither string is empty ("") + * - Neither string contains only whitespace characters (spaces, tabs, newlines, etc.) + * - Both strings are equal ignoring case differences + * + * @param other The other nullable string to compare with this string + * @return `true` if both strings are valid and equal ignoring case differences, `false` otherwise + */ +fun String?.isNotBlankAndEquals(other: String?): Boolean = this != null && + other != null && + this.isNotBlank() && + other.isNotBlank() && + this.equals(other, ignoreCase = true) + +fun String.truncateWithEllipsis(limit: Int) = take(limit) + if (length > limit) StringConstants.THREE_DOT else "" + +object StringConstants { + const val SLASH = "/" + const val DOT = "." + const val SPACE = " " + const val THREE_DOT = "..." + const val TEMP = "tmp" +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/SyncedFolderExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/SyncedFolderExtensions.kt new file mode 100644 index 0000000..5251aa3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/SyncedFolderExtensions.kt @@ -0,0 +1,91 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.nextcloud.client.device.PowerManagementService +import com.nextcloud.client.jobs.BackgroundJobManagerImpl +import com.nextcloud.client.network.ConnectivityService +import com.owncloud.android.R +import com.owncloud.android.datamodel.SyncedFolder +import com.owncloud.android.datamodel.SyncedFolderDisplayItem +import com.owncloud.android.lib.common.utils.Log_OC +import java.io.File + +private const val TAG = "SyncedFolderExtensions" + +/** + * Determines whether a file should be skipped during auto-upload based on folder settings. + */ +@Suppress("ReturnCount") +fun SyncedFolder.shouldSkipFile(file: File, lastModified: Long, creationTime: Long?): Boolean { + if (isExcludeHidden && file.isHidden) { + Log_OC.d(TAG, "Skipping hidden: ${file.absolutePath}") + return true + } + + // If "upload existing files" is DISABLED, only upload files created after enabled time + if (!isExisting) { + if (creationTime != null) { + if (creationTime < enabledTimestampMs) { + Log_OC.d(TAG, "Skipping pre-existing file (creation < enabled): ${file.absolutePath}") + return true + } + } else { + Log_OC.w(TAG, "file sent for upload - cannot determine creation time: ${file.absolutePath}") + return false + } + } + + // Skip files that haven't changed since last scan (already processed) + // BUT only if this is not the first scan + if (lastScanTimestampMs != -1L && lastModified < lastScanTimestampMs) { + Log_OC.d(TAG, "Skipping unchanged file (last modified < last scan): ${file.absolutePath}") + return true + } + + return false +} + +fun List.filterEnabledOrWithoutEnabledParent(): List = filter { + it.isEnabled || !hasEnabledParent(it.localPath) +} + +@Suppress("ReturnCount") +fun List.hasEnabledParent(localPath: String?): Boolean { + localPath ?: return false + + val localFile = File(localPath).takeIf { it.exists() } ?: return false + val parent = localFile.parentFile ?: return false + + return any { it.isEnabled && File(it.localPath).exists() && File(it.localPath) == parent } || + hasEnabledParent(parent.absolutePath) +} + +@Suppress("MagicNumber", "ReturnCount") +fun SyncedFolder.calculateScanInterval( + connectivityService: ConnectivityService, + powerManagementService: PowerManagementService +): Pair { + val defaultIntervalMillis = BackgroundJobManagerImpl.DEFAULT_PERIODIC_JOB_INTERVAL_MINUTES * 60_000L + + if (!connectivityService.isConnected() || connectivityService.isInternetWalled()) { + return defaultIntervalMillis * 2 to null + } + + if (isWifiOnly && !connectivityService.getConnectivity().isWifi) { + return defaultIntervalMillis * 4 to R.string.auto_upload_wifi_only_warning_info + } + + val batteryLevel = powerManagementService.battery.level + return when { + batteryLevel < 20 -> defaultIntervalMillis * 8 to R.string.auto_upload_low_battery_warning_info + batteryLevel < 50 -> defaultIntervalMillis * 4 to R.string.auto_upload_low_battery_warning_info + batteryLevel < 80 -> defaultIntervalMillis * 2 to null + else -> defaultIntervalMillis to null + } +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/TextViewExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/TextViewExtensions.kt index ae54869..11e622f 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/TextViewExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/TextViewExtensions.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.utils.extensions diff --git a/app/src/main/java/com/nextcloud/utils/extensions/ThumbnailsCacheManagerExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/ThumbnailsCacheManagerExtensions.kt new file mode 100644 index 0000000..962c586 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/ThumbnailsCacheManagerExtensions.kt @@ -0,0 +1,77 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import android.provider.MediaStore +import androidx.exifinterface.media.ExifInterface +import androidx.core.net.toUri +import com.owncloud.android.MainApp +import com.owncloud.android.lib.common.utils.Log_OC + +/** + * Retrieves the orientation of an image file from its EXIF metadata or, as a fallback, + * from the Android MediaStore. + * + * This function first attempts to read the orientation using [ExifInterface.TAG_ORIENTATION] + * directly from the file at the given [path]. If that fails or returns + * [ExifInterface.ORIENTATION_UNDEFINED], it then queries the MediaStore for the image's + * stored orientation in degrees (0, 90, 180, or 270), converting that to an EXIF-compatible + * orientation constant. + * + * @param path Absolute file path or content URI (as string) of the image. + * @return One of the [ExifInterface] orientation constants, e.g. + * [ExifInterface.ORIENTATION_ROTATE_90], or [ExifInterface.ORIENTATION_UNDEFINED] + * if the orientation could not be determined. + * + * @see ExifInterface + * @see MediaStore.Images.Media.ORIENTATION + */ +@Suppress("TooGenericExceptionCaught", "NestedBlockDepth", "MagicNumber") +fun getExifOrientation(path: String): Int { + val context = MainApp.getAppContext() + if (context == null || path.isBlank()) { + return ExifInterface.ORIENTATION_UNDEFINED + } + + var orientation = ExifInterface.ORIENTATION_UNDEFINED + + try { + val exif = ExifInterface(path) + orientation = exif.getAttributeInt( + ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_UNDEFINED + ) + } catch (e: Exception) { + Log_OC.e("ThumbnailsCacheManager", "getExifOrientation exception: $e") + } + + // Fallback: query MediaStore if EXIF is undefined + if (orientation == ExifInterface.ORIENTATION_UNDEFINED) { + try { + val uri = path.toUri() + val projection = arrayOf(MediaStore.Images.Media.ORIENTATION) + + context.contentResolver.query(uri, projection, null, null, null)?.use { cursor -> + if (cursor.moveToFirst()) { + val orientationIndex = cursor.getColumnIndexOrThrow(projection[0]) + val degrees = cursor.getInt(orientationIndex) + orientation = when (degrees) { + 90 -> ExifInterface.ORIENTATION_ROTATE_90 + 180 -> ExifInterface.ORIENTATION_ROTATE_180 + 270 -> ExifInterface.ORIENTATION_ROTATE_270 + else -> ExifInterface.ORIENTATION_NORMAL + } + } + } + } catch (e: Exception) { + Log_OC.e("ThumbnailsCacheManager", "getExifOrientation exception: $e") + } + } + + return orientation +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/UploadStorageManagerExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/UploadStorageManagerExtensions.kt new file mode 100644 index 0000000..e86a9f0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/UploadStorageManagerExtensions.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.nextcloud.client.database.entity.UploadEntity +import com.owncloud.android.datamodel.UploadsStorageManager + +fun UploadsStorageManager.updateStatus(entity: UploadEntity?, status: UploadsStorageManager.UploadStatus) { + entity ?: return + uploadDao.insertOrReplace(entity.withStatus(status)) +} + +fun UploadsStorageManager.updateStatus(entity: UploadEntity?, success: Boolean) { + entity ?: return + val newStatus = if (success) { + UploadsStorageManager.UploadStatus.UPLOAD_SUCCEEDED + } else { + UploadsStorageManager.UploadStatus.UPLOAD_FAILED + } + uploadDao.insertOrReplace(entity.withStatus(newStatus)) +} + +private fun UploadEntity.withStatus(newStatus: UploadsStorageManager.UploadStatus) = this.copy(status = newStatus.value) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/UriExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/UriExtensions.kt new file mode 100644 index 0000000..174cf97 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/UriExtensions.kt @@ -0,0 +1,43 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import android.content.Context +import android.net.Uri +import android.provider.MediaStore +import com.owncloud.android.lib.common.utils.Log_OC + +/** + * Returns absolute filesystem path to the media item on disk. I/O errors that could occur. From Android 11 onwards, + * this column is read-only for apps that target R and higher. + * + * [More Info](https://developer.android.com/reference/android/provider/MediaStore.MediaColumns#DATA) + */ +@Suppress("ReturnCount", "TooGenericExceptionCaught") +fun Uri.toFilePath(context: Context): String? { + try { + val projection = arrayOf(MediaStore.MediaColumns.DATA) + + val resolver = context.contentResolver + + resolver.query(this, projection, null, null, null)?.use { cursor -> + if (!cursor.moveToFirst()) { + return null + } + + val dataIdx = cursor.getColumnIndex(MediaStore.MediaColumns.DATA) + val data = if (dataIdx != -1) cursor.getString(dataIdx) else null + return data + } + + return null + } catch (e: Exception) { + Log_OC.e("UriExtensions", "exception, toFilePath: $e") + return null + } +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/ViewExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/ViewExtensions.kt index 9031271..36195f7 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/ViewExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/ViewExtensions.kt @@ -1,20 +1,76 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.utils.extensions +import android.animation.Animator +import android.animation.AnimatorListenerAdapter import android.content.Context import android.graphics.Outline import android.util.TypedValue import android.view.View +import android.view.ViewGroup import android.view.ViewOutlineProvider +import androidx.coordinatorlayout.widget.CoordinatorLayout +import com.nextcloud.ui.behavior.OnScrollBehavior +import com.owncloud.android.lib.common.utils.Log_OC -fun createRoundedOutline(context: Context, cornerRadiusValue: Float): ViewOutlineProvider { - return object : ViewOutlineProvider() { +fun View?.setVisibleIf(condition: Boolean) { + if (this == null) return + visibility = if (condition) View.VISIBLE else View.GONE +} + +fun View?.setVisibilityWithAnimation(condition: Boolean, duration: Long = 200L) { + this ?: return + + if (condition) { + this.apply { + alpha = 0f + visibility = View.VISIBLE + animate() + .alpha(1f) + .setDuration(duration) + .setListener(null) + } + } else { + animate() + .alpha(0f) + .setDuration(duration) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + visibility = View.GONE + } + }) + } +} + +fun View?.makeRounded(context: Context, cornerRadius: Float) { + this?.let { + it.apply { + outlineProvider = createRoundedOutline(context, cornerRadius) + clipToOutline = true + } + } +} + +fun View?.setMargins(left: Int, top: Int, right: Int, bottom: Int) { + if (this == null) { + return + } + + if (layoutParams is ViewGroup.MarginLayoutParams) { + val param = layoutParams as ViewGroup.MarginLayoutParams + param.setMargins(left, top, right, bottom) + requestLayout() + } +} + +fun createRoundedOutline(context: Context, cornerRadiusValue: Float): ViewOutlineProvider = + object : ViewOutlineProvider() { override fun getOutline(view: View, outline: Outline) { val left = 0 val top = 0 @@ -29,4 +85,21 @@ fun createRoundedOutline(context: Context, cornerRadiusValue: Float): ViewOutlin outline.setRoundRect(left, top, right, bottom, cornerRadius.toFloat()) } } + +@Suppress("UNCHECKED_CAST", "ReturnCount", "TooGenericExceptionCaught") +fun T.slideHideBottomBehavior(visible: Boolean) { + this ?: return + val params = layoutParams as? CoordinatorLayout.LayoutParams ?: return + val behavior = params.behavior as? OnScrollBehavior ?: return + post { + try { + if (visible) { + behavior.slideIn(this) + } else { + behavior.slideOut(this) + } + } catch (e: Exception) { + Log_OC.e("slideHideBottomBehavior", e.message) + } + } } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/WorkManagerExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/WorkManagerExtensions.kt index efd20b1..9a3a8b6 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/WorkManagerExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/WorkManagerExtensions.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.utils.extensions @@ -13,23 +13,24 @@ import com.google.common.util.concurrent.ListenableFuture import com.owncloud.android.lib.common.utils.Log_OC import java.util.concurrent.ExecutionException -fun WorkManager.isWorkScheduled(tag: String): Boolean { - val statuses: ListenableFuture> = this.getWorkInfosByTag(tag) - var running = false +private const val TAG = "WorkManager" + +fun WorkManager.isWorkRunning(tag: String): Boolean = checkWork(tag, listOf(WorkInfo.State.RUNNING)) + +fun WorkManager.isWorkScheduled(tag: String): Boolean = + checkWork(tag, listOf(WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED)) + +private fun WorkManager.checkWork(tag: String, stateConditions: List): Boolean { + val statuses: ListenableFuture> = getWorkInfosByTag(tag) var workInfoList: List = emptyList() try { workInfoList = statuses.get() } catch (e: ExecutionException) { - Log_OC.d("Worker", "ExecutionException in isWorkScheduled: $e") + Log_OC.d(TAG, "ExecutionException in checkWork: $e") } catch (e: InterruptedException) { - Log_OC.d("Worker", "InterruptedException in isWorkScheduled: $e") + Log_OC.d(TAG, "InterruptedException in checkWork: $e") } - for (workInfo in workInfoList) { - val state = workInfo.state - running = running || (state == WorkInfo.State.RUNNING || state == WorkInfo.State.ENQUEUED) - } - - return running + return workInfoList.any { workInfo -> stateConditions.contains(workInfo.state) } } diff --git a/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt new file mode 100644 index 0000000..dd1338e --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/fileNameValidator/FileNameValidator.kt @@ -0,0 +1,153 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.fileNameValidator + +import android.content.Context +import android.text.TextUtils +import com.nextcloud.utils.extensions.StringConstants +import com.nextcloud.utils.extensions.checkWCFRestrictions +import com.nextcloud.utils.extensions.forbiddenFilenameBaseNames +import com.nextcloud.utils.extensions.forbiddenFilenameCharacters +import com.nextcloud.utils.extensions.forbiddenFilenameExtensions +import com.nextcloud.utils.extensions.forbiddenFilenames +import com.nextcloud.utils.extensions.removeFileExtension +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.resources.status.OCCapability + +object FileNameValidator { + + /** + * Checks the validity of a file name. + * + * @param filename The name of the file to validate. + * @param capability The capabilities affecting the validation criteria + * such as forbiddenFilenames, forbiddenCharacters. + * @param context The context used for retrieving error messages. + * @param existedFileNames Set of existing file names to avoid duplicates. + * @return An error message if the filename is invalid, null otherwise. + */ + @Suppress("ReturnCount", "NestedBlockDepth") + fun checkFileName( + filename: String, + capability: OCCapability, + context: Context, + existedFileNames: Set? = null + ): String? { + if (filename.isBlank()) { + return context.getString(R.string.filename_empty) + } + + existedFileNames?.let { + if (isFileNameAlreadyExist(filename, existedFileNames)) { + return context.getString(R.string.file_already_exists) + } + } + + if (!capability.checkWCFRestrictions()) { + return null + } + + // region WCF related checks + checkInvalidCharacters(filename, capability, context)?.let { return it } + + val filenameVariants = setOf(filename.lowercase(), filename.removeFileExtension().lowercase()) + + with(capability) { + forbiddenFilenameBaseNamesJson?.let { + forbiddenFilenameBaseNames().find { it.lowercase() in filenameVariants }?.let { forbiddenBaseFilename -> + return context.getString(R.string.file_name_validator_error_reserved_names, forbiddenBaseFilename) + } + } + + forbiddenFilenamesJson?.let { + forbiddenFilenames().find { it.lowercase() in filenameVariants }?.let { forbiddenFilename -> + return context.getString(R.string.file_name_validator_error_reserved_names, forbiddenFilename) + } + } + + forbiddenFilenameExtensionJson?.let { + forbiddenFilenameExtensions().find { extension -> + when { + extension == StringConstants.SPACE -> + filename.startsWith(extension, ignoreCase = true) || + filename.endsWith(extension, ignoreCase = true) + + else -> filename.endsWith(extension, ignoreCase = true) + } + }?.let { forbiddenExtension -> + return if (forbiddenExtension == StringConstants.SPACE) { + context.getString(R.string.file_name_validator_error_forbidden_space_character_extensions) + } else { + context.getString( + R.string.file_name_validator_error_forbidden_file_extensions, + forbiddenExtension + ) + } + } + } + } + // endregion + + return null + } + + /** + * Checks the validity of file paths wanted to move or copied inside the folder. + * + * @param folderPath Target folder to be used for move or copy. + * @param filePaths The list of file paths to move or copy to folderPath. + * @param capability The capabilities affecting the validation criteria. + * @param context The context used for retrieving error messages. + * @return True if folder path and file paths are valid, false otherwise. + */ + fun checkFolderAndFilePaths( + folderPath: String, + filePaths: List, + capability: OCCapability, + context: Context + ): Boolean = checkFolderPath(folderPath, capability, context) && checkFilePaths(filePaths, capability, context) + + fun checkParentRemotePaths(filePaths: List, capability: OCCapability, context: Context): Boolean = + filePaths.all { + if (it.parentRemotePath != StringConstants.SLASH) { + val parentFolderName = it.parentRemotePath.replace(StringConstants.SLASH, "") + checkFileName(parentFolderName, capability, context) == null + } else { + true + } + } + + private fun checkFilePaths(filePaths: List, capability: OCCapability, context: Context): Boolean = + filePaths.all { + checkFileName(it, capability, context) == null + } + + fun checkFolderPath(folderPath: String, capability: OCCapability, context: Context): Boolean = + folderPath.split("[/\\\\]".toRegex()) + .none { it.isNotEmpty() && checkFileName(it, capability, context) != null } + + @Suppress("ReturnCount") + private fun checkInvalidCharacters(name: String, capability: OCCapability, context: Context): String? { + capability.forbiddenFilenameCharactersJson?.let { + val forbiddenFilenameCharacters = capability.forbiddenFilenameCharacters() + + val invalidCharacter = forbiddenFilenameCharacters.firstOrNull { name.contains(it) } + + if (invalidCharacter == null) return null + + return context.getString(R.string.file_name_validator_error_invalid_character, invalidCharacter) + } + + return null + } + + fun isFileHidden(name: String): Boolean = !TextUtils.isEmpty(name) && name[0] == '.' + + fun isFileNameAlreadyExist(name: String, fileNames: Set): Boolean = fileNames.contains(name) +} diff --git a/app/src/main/java/com/nextcloud/utils/mdm/MDMConfig.kt b/app/src/main/java/com/nextcloud/utils/mdm/MDMConfig.kt new file mode 100644 index 0000000..8443636 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/mdm/MDMConfig.kt @@ -0,0 +1,136 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.mdm + +import android.content.Context +import android.content.RestrictionsManager +import com.owncloud.android.R +import com.owncloud.android.utils.appConfig.AppConfigKeys + +object MDMConfig { + fun multiAccountSupport(context: Context): Boolean { + val multiAccountSupport = context.resources.getBoolean(R.bool.multiaccount_support) + + val disableMultiAccountViaMDM = context.getRestriction( + AppConfigKeys.DisableMultiAccount, + context.resources.getBoolean(R.bool.disable_multiaccount) + ) + + return multiAccountSupport && !disableMultiAccountViaMDM + } + + fun shareViaLink(context: Context): Boolean { + val disableShareViaMDM = context.getRestriction( + AppConfigKeys.DisableSharing, + context.resources.getBoolean(R.bool.disable_sharing) + ) + + val shareViaLink = context.resources.getBoolean(R.bool.share_via_link_feature) + + return shareViaLink && !disableShareViaMDM + } + + fun shareViaUser(context: Context): Boolean { + val disableShareViaMDM = context.getRestriction( + AppConfigKeys.DisableSharing, + context.resources.getBoolean(R.bool.disable_sharing) + ) + + val shareViaUsers = context.resources.getBoolean(R.bool.share_with_users_feature) + + return shareViaUsers && !disableShareViaMDM + } + + fun sendFilesSupport(context: Context): Boolean { + val disableShareViaMDM = context.getRestriction( + AppConfigKeys.DisableSharing, + context.resources.getBoolean(R.bool.disable_sharing) + ) + + val sendFilesToOtherApp = "on".equals(context.getString(R.string.send_files_to_other_apps), ignoreCase = true) + + return sendFilesToOtherApp && !disableShareViaMDM + } + + fun sharingSupport(context: Context): Boolean { + val disableShareViaMDM = context.getRestriction( + AppConfigKeys.DisableSharing, + context.resources.getBoolean(R.bool.disable_sharing) + ) + + val sendFilesToOtherApp = "on".equals(context.getString(R.string.send_files_to_other_apps), ignoreCase = true) + + val shareViaUsers = context.resources.getBoolean(R.bool.share_with_users_feature) + + val shareViaLink = context.resources.getBoolean(R.bool.share_via_link_feature) + + return sendFilesToOtherApp && shareViaLink && shareViaUsers && !disableShareViaMDM + } + + fun clipBoardSupport(context: Context): Boolean { + val disableClipboardSupport = context.getRestriction( + AppConfigKeys.DisableClipboard, + context.resources.getBoolean(R.bool.disable_clipboard) + ) + + return !disableClipboardSupport + } + + fun externalSiteSupport(context: Context): Boolean { + val disableMoreExternalSiteViaMDM = context.getRestriction( + AppConfigKeys.DisableMoreExternalSite, + context.resources.getBoolean(R.bool.disable_more_external_site) + ) + + val showExternalLinks = context.resources.getBoolean(R.bool.show_external_links) + + return showExternalLinks && !disableMoreExternalSiteViaMDM + } + + fun showIntro(context: Context): Boolean { + val disableIntroViaMDM = + context.getRestriction(AppConfigKeys.DisableIntro, context.resources.getBoolean(R.bool.disable_intro)) + + val isProviderOrOwnInstallationVisible = context.resources.getBoolean(R.bool.show_provider_or_own_installation) + + return isProviderOrOwnInstallationVisible && !disableIntroViaMDM + } + + fun isLogEnabled(context: Context): Boolean { + val disableLogViaMDM = + context.getRestriction(AppConfigKeys.DisableLog, context.resources.getBoolean(R.bool.disable_log)) + + val loggerEnabled = context.resources.getBoolean(R.bool.logger_enabled) + + return loggerEnabled && !disableLogViaMDM + } + + fun getBaseUrl(context: Context): String = context.getRestriction(AppConfigKeys.BaseUrl, "") + + fun getHost(context: Context): String = + context.getRestriction(AppConfigKeys.ProxyHost, context.getString(R.string.proxy_host)) + + fun getPort(context: Context): Int = + context.getRestriction(AppConfigKeys.ProxyPort, context.resources.getInteger(R.integer.proxy_port)) + + fun enforceProtection(context: Context): Boolean = + context.getRestriction(AppConfigKeys.EnforceProtection, context.resources.getBoolean(R.bool.enforce_protection)) + + @Suppress("UNCHECKED_CAST") + private fun Context.getRestriction(appConfigKey: AppConfigKeys, defaultValue: T): T { + val restrictionsManager = getSystemService(Context.RESTRICTIONS_SERVICE) as? RestrictionsManager + val appRestrictions = restrictionsManager?.getApplicationRestrictions() ?: return defaultValue + + return when (defaultValue) { + is String -> appRestrictions.getString(appConfigKey.key, defaultValue) as T? ?: defaultValue + is Int -> appRestrictions.getInt(appConfigKey.key, defaultValue) as T? ?: defaultValue + is Boolean -> appRestrictions.getBoolean(appConfigKey.key, defaultValue) as T? ?: defaultValue + else -> defaultValue + } + } +} diff --git a/app/src/main/java/com/nextcloud/utils/numberFormatter/NumberFormatter.kt b/app/src/main/java/com/nextcloud/utils/numberFormatter/NumberFormatter.kt new file mode 100644 index 0000000..d036152 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/numberFormatter/NumberFormatter.kt @@ -0,0 +1,21 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.numberFormatter + +import java.text.NumberFormat +import java.util.Locale + +object NumberFormatter { + + @Suppress("MagicNumber") + fun getPercentageText(percent: Int): String { + val formatter = NumberFormat.getPercentInstance(Locale.getDefault()) + formatter.maximumFractionDigits = 0 + return formatter.format(percent / 100.0) + } +} diff --git a/app/src/main/java/com/nextcloud/utils/view/FastScrollPopupBackground.kt b/app/src/main/java/com/nextcloud/utils/view/FastScrollPopupBackground.kt index 430bc99..8d716f6 100644 --- a/app/src/main/java/com/nextcloud/utils/view/FastScrollPopupBackground.kt +++ b/app/src/main/java/com/nextcloud/utils/view/FastScrollPopupBackground.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.utils.view @@ -58,17 +58,11 @@ class FastScrollPopupBackground(context: Context, @ColorInt color: Int) : Drawab // noop } - override fun isAutoMirrored(): Boolean { - return true - } + override fun isAutoMirrored(): Boolean = true - override fun getOpacity(): Int { - return PixelFormat.TRANSPARENT - } + override fun getOpacity(): Int = PixelFormat.TRANSPARENT - private fun shouldMirrorPath(): Boolean { - return DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL - } + private fun shouldMirrorPath(): Boolean = DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL override fun onLayoutDirectionChanged(layoutDirection: Int): Boolean { updatePath() diff --git a/app/src/main/java/com/nextcloud/utils/view/FastScrollUtils.kt b/app/src/main/java/com/nextcloud/utils/view/FastScrollUtils.kt index 34affcf..807d4b3 100644 --- a/app/src/main/java/com/nextcloud/utils/view/FastScrollUtils.kt +++ b/app/src/main/java/com/nextcloud/utils/view/FastScrollUtils.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nextcloud.utils.view @@ -17,10 +17,7 @@ import javax.inject.Inject class FastScrollUtils @Inject constructor(private val viewThemeUtils: ViewThemeUtils) { @JvmOverloads - fun applyFastScroll( - recyclerView: RecyclerView, - viewHelper: FastScroller.ViewHelper? = null - ) { + fun applyFastScroll(recyclerView: RecyclerView, viewHelper: FastScroller.ViewHelper? = null) { val builder = FastScrollerBuilder(recyclerView).let { viewThemeUtils.files.themeFastScrollerBuilder( diff --git a/app/src/main/java/com/nmc/android/ui/LauncherActivity.kt b/app/src/main/java/com/nmc/android/ui/LauncherActivity.kt index db20687..b39b3f8 100644 --- a/app/src/main/java/com/nmc/android/ui/LauncherActivity.kt +++ b/app/src/main/java/com/nmc/android/ui/LauncherActivity.kt @@ -1,10 +1,10 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Andy Scherzinger - * SPDX-FileCopyrightText: 2023 TSI-mc - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2023-2024 TSI-mc + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.nmc.android.ui @@ -17,10 +17,13 @@ import android.view.View import androidx.annotation.VisibleForTesting import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import com.nextcloud.client.preferences.AppPreferences +import com.nextcloud.utils.mdm.MDMConfig import com.owncloud.android.R +import com.owncloud.android.authentication.AuthenticatorActivity import com.owncloud.android.databinding.ActivitySplashBinding import com.owncloud.android.ui.activity.BaseActivity import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.activity.SettingsActivity import javax.inject.Inject class LauncherActivity : BaseActivity() { @@ -64,7 +67,13 @@ class LauncherActivity : BaseActivity() { private fun scheduleSplashScreen() { Handler(Looper.getMainLooper()).postDelayed({ if (user.isPresent) { - startActivity(Intent(this, FileDisplayActivity::class.java)) + if (MDMConfig.enforceProtection(this) && appPreferences.lockPreference == SettingsActivity.LOCK_NONE) { + startActivity(Intent(this, SettingsActivity::class.java)) + } else { + startActivity(Intent(this, FileDisplayActivity::class.java)) + } + } else { + startActivity(Intent(this, AuthenticatorActivity::class.java)) } finish() }, SPLASH_DURATION) diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java index f2b4d2e..64c8f48 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -1,7 +1,7 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 TSI-mc * SPDX-FileCopyrightText: 2022-2023 Álvaro Brey * SPDX-FileCopyrightText: 2016-2020 Tobias Kaminsky @@ -11,7 +11,7 @@ * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2014 David A. Velasco * SPDX-FileCopyrightText: 2013 María Asensio Valverde - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android; @@ -21,13 +21,16 @@ import android.app.ActivityManager; import android.app.Application; import android.app.NotificationChannel; import android.app.NotificationManager; +import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.content.pm.ActivityInfo; +import android.content.IntentFilter; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.Resources; +import android.net.ConnectivityManager; import android.os.Build; import android.os.Bundle; import android.os.Environment; @@ -35,6 +38,7 @@ import android.os.StrictMode; import android.text.TextUtils; import android.view.WindowManager; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.nextcloud.appReview.InAppReviewHelper; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; @@ -55,14 +59,18 @@ import com.nextcloud.client.onboarding.OnboardingService; import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.client.preferences.AppPreferencesImpl; import com.nextcloud.client.preferences.DarkMode; +import com.nextcloud.receiver.NetworkChangeListener; +import com.nextcloud.receiver.NetworkChangeReceiver; +import com.nextcloud.utils.extensions.ContextExtensionsKt; +import com.nextcloud.utils.mdm.MDMConfig; import com.nmc.android.ui.LauncherActivity; -import com.owncloud.android.authentication.AuthenticatorActivity; import com.owncloud.android.authentication.PassCodeManager; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; import com.owncloud.android.datamodel.MediaFolder; import com.owncloud.android.datamodel.MediaFolderType; import com.owncloud.android.datamodel.MediaProvider; +import com.owncloud.android.datamodel.ReceiverFlag; import com.owncloud.android.datamodel.SyncedFolder; import com.owncloud.android.datamodel.SyncedFolderProvider; import com.owncloud.android.datamodel.ThumbnailsCacheManager; @@ -103,7 +111,6 @@ import javax.net.ssl.SSLEngine; import androidx.annotation.NonNull; import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatDelegate; import androidx.core.util.Pair; import androidx.lifecycle.Lifecycle; @@ -112,8 +119,6 @@ import androidx.lifecycle.ProcessLifecycleOwner; import dagger.android.AndroidInjector; import dagger.android.DispatchingAndroidInjector; import dagger.android.HasAndroidInjector; -import de.cotech.hw.SecurityKeyManager; -import de.cotech.hw.SecurityKeyManagerConfig; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import static com.owncloud.android.ui.activity.ContactsPreferenceActivity.PREFERENCE_CONTACTS_AUTOMATIC_BACKUP; @@ -123,9 +128,9 @@ import static com.owncloud.android.ui.activity.ContactsPreferenceActivity.PREFER * Main Application of the project. * Contains methods to build the "static" strings. These strings were before constants in different classes. */ -public class MainApp extends Application implements HasAndroidInjector { - public static final OwnCloudVersion OUTDATED_SERVER_VERSION = NextcloudVersion.nextcloud_26; - public static final OwnCloudVersion MINIMUM_SUPPORTED_SERVER_VERSION = OwnCloudVersion.nextcloud_16; +public class MainApp extends Application implements HasAndroidInjector, NetworkChangeListener { + public static final OwnCloudVersion OUTDATED_SERVER_VERSION = NextcloudVersion.nextcloud_29; + public static final OwnCloudVersion MINIMUM_SUPPORTED_SERVER_VERSION = OwnCloudVersion.nextcloud_20; private static final String TAG = MainApp.class.getSimpleName(); public static final String DOT = "."; @@ -137,6 +142,7 @@ public class MainApp extends Application implements HasAndroidInjector { private static boolean mOnlyOnDevice; private static boolean mOnlyPersonalFiles; + @Inject protected AppPreferences preferences; @@ -155,6 +161,9 @@ public class MainApp extends Application implements HasAndroidInjector { @Inject ConnectivityService connectivityService; + @Inject + SyncedFolderProvider syncedFolderProvider; + @Inject PowerManagementService powerManagementService; @Inject @@ -193,6 +202,8 @@ public class MainApp extends Application implements HasAndroidInjector { private static AppComponent appComponent; + private NetworkChangeReceiver networkChangeReceiver; + /** * Temporary hack */ @@ -216,6 +227,11 @@ public class MainApp extends Application implements HasAndroidInjector { return powerManagementService; } + private void registerNetworkChangeReceiver() { + IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); + registerReceiver(networkChangeReceiver, filter); + } + private String getAppProcessName() { String processName = ""; if(Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { @@ -281,6 +297,7 @@ public class MainApp extends Application implements HasAndroidInjector { return appComponent; } + @SuppressFBWarnings("ST") @Override public void onCreate() { @@ -295,8 +312,6 @@ public class MainApp extends Application implements HasAndroidInjector { insertConscrypt(); - initSecurityKeyManager(); - registerActivityLifecycleCallbacks(new ActivityInjector()); //update the app restart count when app is launched by the user @@ -310,22 +325,24 @@ public class MainApp extends Application implements HasAndroidInjector { fixStoragePath(); + checkCancelDownloadJobs(); + MainApp.storagePath = preferences.getStoragePath(getApplicationContext().getFilesDir().getAbsolutePath()); OwnCloudClientManagerFactory.setUserAgent(getUserAgent()); - try { - OwnCloudClientManagerFactory.setProxyHost(getResources().getString(R.string.proxy_host)); - OwnCloudClientManagerFactory.setProxyPort(getResources().getInteger(R.integer.proxy_port)); - } catch (Resources.NotFoundException e) { - // no proxy set + if (isClientBrandedPlus()) { + setProxyConfig(); + ContextExtensionsKt.registerBroadcastReceiver(this, restrictionsReceiver, restrictionsFilter, ReceiverFlag.NotExported); + } else { + setProxyForNonBrandedPlusClients(); } // initialise thumbnails cache on background thread - new ThumbnailsCacheManager.InitDiskCacheTask().execute(); + ThumbnailsCacheManager.initDiskCacheAsync(); - if (BuildConfig.DEBUG || getApplicationContext().getResources().getBoolean(R.bool.logger_enabled)) { + if (MDMConfig.INSTANCE.isLogEnabled(this)) { // use app writable dir, no permissions needed Log_OC.setLoggerImplementation(new LegacyLoggerAdapter(logger)); Log_OC.d("Debug", "start logging"); @@ -337,8 +354,8 @@ public class MainApp extends Application implements HasAndroidInjector { } catch (Exception e) { Log_OC.d("Debug", "Failed to disable uri exposure"); } - - initSyncOperations(preferences, + initSyncOperations(this, + preferences, uploadsStorageManager, accountManager, connectivityService, @@ -346,16 +363,40 @@ public class MainApp extends Application implements HasAndroidInjector { backgroundJobManager, clock, viewThemeUtils, - walledCheckCache); + walledCheckCache, + syncedFolderProvider); initContactsBackup(accountManager, backgroundJobManager); notificationChannels(); - backgroundJobManager.scheduleMediaFoldersDetectionJob(); - backgroundJobManager.startMediaFoldersDetectionJob(); + if (backgroundJobManager != null) { + backgroundJobManager.scheduleMediaFoldersDetectionJob(); + backgroundJobManager.startMediaFoldersDetectionJob(); + backgroundJobManager.schedulePeriodicHealthStatus(); - backgroundJobManager.schedulePeriodicHealthStatus(); + if (preferences.isTwoWaySyncEnabled()) { + backgroundJobManager.scheduleInternal2WaySync(preferences.getTwoWaySyncInterval()); + } + + backgroundJobManager.startPeriodicallyOfflineOperation(); + } registerGlobalPassCodeProtection(); + networkChangeReceiver = new NetworkChangeReceiver(this, connectivityService); + registerNetworkChangeReceiver(); + + if (!MDMConfig.INSTANCE.sendFilesSupport(this)) { + disableDocumentsStorageProvider(); + } + + + } + + public void disableDocumentsStorageProvider() { + String packageName = getPackageName(); + String providerClassName = "com.owncloud.android.providers.DocumentsStorageProvider"; + ComponentName componentName = new ComponentName(packageName, providerClassName); + PackageManager packageManager = getPackageManager(); + packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); } private final LifecycleEventObserver lifecycleEventObserver = ((lifecycleOwner, event) -> { @@ -364,9 +405,61 @@ public class MainApp extends Application implements HasAndroidInjector { } else if (event == Lifecycle.Event.ON_STOP) { passCodeManager.setCanAskPin(true); Log_OC.d(TAG, "APP IN BACKGROUND"); + } else if (event == Lifecycle.Event.ON_RESUME) { + setProxyConfig(); + Log_OC.d(TAG, "APP ON RESUME"); } }); + private void setProxyForNonBrandedPlusClients() { + try { + OwnCloudClientManagerFactory.setProxyHost(getResources().getString(R.string.proxy_host)); + OwnCloudClientManagerFactory.setProxyPort(getResources().getInteger(R.integer.proxy_port)); + } catch (Resources.NotFoundException e) { + Log_OC.d(TAG, "Error caught at setProxyForNonBrandedPlusClients: " + e); + } + } + + public static boolean isClientBranded() { + return getAppContext().getResources().getBoolean(R.bool.is_branded_client); + } + + public static boolean isClientBrandedPlus() { + return getAppContext().getResources().getBoolean(R.bool.is_branded_plus_client); + } + + private final IntentFilter restrictionsFilter = new IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED); + + private final BroadcastReceiver restrictionsReceiver = new BroadcastReceiver() { + @Override public void onReceive(Context context, Intent intent) { + setProxyConfig(); + } + }; + + private void setProxyConfig() { + if (!isClientBrandedPlus()) { + Log_OC.d(TAG, "Proxy configuration cannot be set. Client is not branded plus."); + return; + } + + String host = MDMConfig.INSTANCE.getHost(this); + int port = MDMConfig.INSTANCE.getPort(this); + + if (TextUtils.isEmpty(host) || port == -1) { + Log_OC.d(TAG, "Proxy configuration cannot be found"); + return; + } + + try { + OwnCloudClientManagerFactory.setProxyHost(host); + OwnCloudClientManagerFactory.setProxyPort(port); + + Log_OC.d(TAG, "Proxy configuration successfully set"); + } catch (Resources.NotFoundException e) { + Log_OC.e(TAG, "Proxy config cannot able to set due to: $e"); + } + } + private void registerGlobalPassCodeProtection() { registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { @@ -418,42 +511,16 @@ public class MainApp extends Application implements HasAndroidInjector { }); } - @SuppressWarnings("unchecked") - private void initSecurityKeyManager() { - SecurityKeyManager securityKeyManager = SecurityKeyManager.getInstance(); - final SecurityKeyManagerConfig.Builder configBuilder = new SecurityKeyManagerConfig.Builder() - .setEnableDebugLogging(BuildConfig.DEBUG); - - try { - // exclude all activities except AuthenticatorActivity - final PackageManager pm = this.getPackageManager(); - final PackageInfo info = pm.getPackageInfo(this.getPackageName(), PackageManager.GET_ACTIVITIES); - final ActivityInfo[] activities = info.activities; - for (ActivityInfo activityInfo : activities) { - try { - final Class aClass = (Class) Class.forName(activityInfo.name); - if (aClass != AuthenticatorActivity.class) { - configBuilder.addExcludedActivityClass(aClass); - } - } catch (ClassNotFoundException | ClassCastException e) { - Log_OC.e(TAG, "Couldn't disable activity for security key listener", e); - } - } - } catch (PackageManager.NameNotFoundException e) { - Log_OC.e(TAG, "Couldn't disable activities for security key listener", e); - } - - - securityKeyManager.init(this, configBuilder.build()); - } - public static void initContactsBackup(UserAccountManager accountManager, BackgroundJobManager backgroundJobManager) { ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(appContext.get()); + if (accountManager == null) { + return; + } + List users = accountManager.getAllUsers(); for (User user : users) { - if (arbitraryDataProvider.getBooleanValue(user, PREFERENCE_CONTACTS_AUTOMATIC_BACKUP)) { + if (backgroundJobManager != null && arbitraryDataProvider.getBooleanValue(user, PREFERENCE_CONTACTS_AUTOMATIC_BACKUP)) { backgroundJobManager.schedulePeriodicContactsBackup(user); - } } } @@ -506,7 +573,6 @@ public class MainApp extends Application implements HasAndroidInjector { if (storagePoint.getPrivacyType() == StoragePoint.PrivacyType.PUBLIC) { preferences.setStoragePath(storagePoint.getPath()); preferences.removeKeysMigrationPreference(); - set = true; break; } } @@ -538,7 +604,15 @@ public class MainApp extends Application implements HasAndroidInjector { } } + private void checkCancelDownloadJobs() { + if (backgroundJobManager != null && preferences.shouldStopDownloadJobsOnStart()) { + backgroundJobManager.cancelAllFilesDownloadJobs(); + preferences.setStopDownloadJobsOnStart(false); + } + } + public static void initSyncOperations( + final Context context, final AppPreferences preferences, final UploadsStorageManager uploadsStorageManager, final UserAccountManager accountManager, @@ -547,8 +621,9 @@ public class MainApp extends Application implements HasAndroidInjector { final BackgroundJobManager backgroundJobManager, final Clock clock, final ViewThemeUtils viewThemeUtils, - final WalledCheckCache walledCheckCache) { - updateToAutoUpload(); + final WalledCheckCache walledCheckCache, + final SyncedFolderProvider syncedFolderProvider) { + updateToAutoUpload(context); cleanOldEntries(clock); updateAutoUploadEntries(clock); @@ -561,12 +636,12 @@ public class MainApp extends Application implements HasAndroidInjector { } if (!preferences.isAutoUploadInitialized()) { - backgroundJobManager.startImmediateFilesSyncJob(false, new String[]{}); + FilesSyncHelper.startAutoUploadImmediately(syncedFolderProvider, backgroundJobManager, false); preferences.setAutoUploadInit(true); } - FilesSyncHelper.scheduleFilesSyncIfNeeded(appContext.get(), backgroundJobManager); - FilesSyncHelper.restartJobsIfNeeded( + FilesSyncHelper.scheduleFilesSyncForAllFoldersIfNeeded(appContext.get(), syncedFolderProvider, backgroundJobManager); + FilesSyncHelper.restartUploadsIfNeeded( uploadsStorageManager, accountManager, connectivityService, @@ -592,7 +667,7 @@ public class MainApp extends Application implements HasAndroidInjector { } public static void notificationChannels() { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O && getAppContext() != null) { + if (getAppContext() != null) { Context context = getAppContext(); NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); @@ -604,7 +679,7 @@ public class MainApp extends Application implements HasAndroidInjector { createChannel(notificationManager, NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD, R.string.notification_channel_upload_name_short, - R.string.notification_channel_upload_description, context); + R.string.notification_channel_upload_description, context, NotificationManager.IMPORTANCE_LOW); createChannel(notificationManager, NotificationUtils.NOTIFICATION_CHANNEL_MEDIA, R.string.notification_channel_media_name, @@ -620,9 +695,24 @@ public class MainApp extends Application implements HasAndroidInjector { R.string.notification_channel_push_name, R.string .notification_channel_push_description, context, NotificationManager.IMPORTANCE_DEFAULT); + createChannel(notificationManager, NotificationUtils.NOTIFICATION_CHANNEL_BACKGROUND_OPERATIONS, + R.string.notification_channel_background_operations_name, R.string + .notification_channel_background_operations_description, context, NotificationManager.IMPORTANCE_LOW); + createChannel(notificationManager, NotificationUtils.NOTIFICATION_CHANNEL_GENERAL, R.string .notification_channel_general_name, R.string.notification_channel_general_description, context, NotificationManager.IMPORTANCE_DEFAULT); + + createChannel(notificationManager, NotificationUtils.NOTIFICATION_CHANNEL_OFFLINE_OPERATIONS, + R.string.notification_channel_offline_operations_name_short, + R.string.notification_channel_offline_operations_description, context); + + createChannel(notificationManager, + NotificationUtils.NOTIFICATION_CHANNEL_CONTENT_OBSERVER, + R.string.notification_channel_content_observer_name_short, + R.string.notification_channel_content_observer_description, + context, + NotificationManager.IMPORTANCE_LOW); } else { Log_OC.e(TAG, "Notification manager is null"); } @@ -639,8 +729,7 @@ public class MainApp extends Application implements HasAndroidInjector { private static void createChannel(NotificationManager notificationManager, String channelId, int channelName, int channelDescription, Context context, int importance) { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O - && getAppContext() != null) { + if (getAppContext() != null) { CharSequence name = context.getString(channelName); String description = context.getString(channelDescription); NotificationChannel channel = new NotificationChannel(channelId, name, importance); @@ -735,6 +824,10 @@ public class MainApp extends Application implements HasAndroidInjector { return getUserAgent(R.string.nextcloud_user_agent); } + public static void showMessage(int messageId) { + ContextExtensionsKt.showToast(getAppContext(), messageId); + } + // user agent private static String getUserAgent(@StringRes int agent) { String appString = string(agent); @@ -754,32 +847,37 @@ public class MainApp extends Application implements HasAndroidInjector { return String.format(appString, version, brandedName); } - private static void updateToAutoUpload() { - Context context = getAppContext(); + private static void updateToAutoUpload(Context context) { AppPreferences preferences = AppPreferencesImpl.fromContext(context); if (preferences.instantPictureUploadEnabled() || preferences.instantVideoUploadEnabled()) { preferences.removeLegacyPreferences(); // show info pop-up try { - new AlertDialog.Builder(context, R.style.Theme_ownCloud_Dialog) - .setTitle(R.string.drawer_synced_folders) - .setMessage(R.string.synced_folders_new_info) - .setPositiveButton(R.string.drawer_open, (dialog, which) -> { - // show Auto Upload - Intent folderSyncIntent = new Intent(context, SyncedFoldersActivity.class); - dialog.dismiss(); - context.startActivity(folderSyncIntent); - }) - .setNegativeButton(R.string.drawer_close, (dialog, which) -> dialog.dismiss()) - .setIcon(R.drawable.nav_synced_folders) - .show(); + showAutoUploadAlertDialog(context); } catch (WindowManager.BadTokenException e) { Log_OC.i(TAG, "Error showing Auto Upload Update dialog, so skipping it: " + e.getMessage()); } } } + + + private static void showAutoUploadAlertDialog(Context context) { + new MaterialAlertDialogBuilder(context, R.style.Theme_ownCloud_Dialog) + .setTitle(R.string.drawer_synced_folders) + .setMessage(R.string.synced_folders_new_info) + .setPositiveButton(R.string.drawer_open, (dialog, which) -> { + Intent folderSyncIntent = new Intent(context, SyncedFoldersActivity.class); + dialog.dismiss(); + context.startActivity(folderSyncIntent); + }) + .setNegativeButton(R.string.drawer_close, (dialog, which) -> dialog.dismiss()) + .setIcon(R.drawable.nav_synced_folders) + .create() + .show(); + } + private static void updateAutoUploadEntries(Clock clock) { // updates entries to reflect their true paths Context context = getAppContext(); @@ -917,4 +1015,22 @@ public class MainApp extends Application implements HasAndroidInjector { case SYSTEM -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); } } + + @Override + public void networkAndServerConnectionListener(boolean isNetworkAndServerAvailable) { + if (backgroundJobManager == null) { + Log_OC.d(TAG, "Offline operations terminated, backgroundJobManager cannot be null"); + return; + } + + if (isNetworkAndServerAvailable) { + backgroundJobManager.startOfflineOperations(); + } + } + + @Override + public void onTerminate() { + super.onTerminate(); + ReceiversHelper.shutdown(); + } } diff --git a/app/src/main/java/com/owncloud/android/authentication/AccountAuthenticator.java b/app/src/main/java/com/owncloud/android/authentication/AccountAuthenticator.java index e7a5572..6f3adf6 100644 --- a/app/src/main/java/com/owncloud/android/authentication/AccountAuthenticator.java +++ b/app/src/main/java/com/owncloud/android/authentication/AccountAuthenticator.java @@ -6,7 +6,7 @@ * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2012 David A. Velasco * SPDX-FileCopyrightText: 2011-2012 Bartosz Przybylski - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.authentication; @@ -21,6 +21,7 @@ import android.os.Bundle; import android.os.Handler; import android.widget.Toast; +import com.nextcloud.utils.mdm.MDMConfig; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.lib.common.accounts.AccountTypeUtils; @@ -70,7 +71,7 @@ public class AccountAuthenticator extends AbstractAccountAuthenticator { final Bundle bundle = new Bundle(); - if (mContext.getResources().getBoolean(R.bool.multiaccount_support) || accounts.length < 1) { + if (accounts.length < 1 || MDMConfig.INSTANCE.multiAccountSupport(mContext)) { try { validateAccountType(accountType); } catch (AuthenticatorException e) { diff --git a/app/src/main/java/com/owncloud/android/authentication/AuthObject.kt b/app/src/main/java/com/owncloud/android/authentication/AuthObject.kt new file mode 100644 index 0000000..eddaecc --- /dev/null +++ b/app/src/main/java/com/owncloud/android/authentication/AuthObject.kt @@ -0,0 +1,12 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.authentication + +data class AuthObject(val poll: Poll, val login: String) + +data class Poll(val token: String, val endpoint: String) diff --git a/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java b/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java index f285b2a..c87c015 100644 --- a/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java +++ b/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java @@ -1,7 +1,7 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 TSI-mc + * SPDX-FileCopyrightText: 2023-2025 TSI-mc * SPDX-FileCopyrightText: 2019-2021 Tobias Kaminsky * SPDX-FileCopyrightText: 2018 Andy Scherzinger * SPDX-FileCopyrightText: 2017 Mario Danic @@ -9,7 +9,7 @@ * SPDX-FileCopyrightText: 2013-2015 María Asensio Valverde * SPDX-FileCopyrightText: 2013-2015 David A. Velasco * SPDX-FileCopyrightText: 2011-2012 Bartosz Przybylski - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.authentication; @@ -17,58 +17,67 @@ import android.Manifest; import android.accounts.Account; import android.accounts.AccountManager; import android.annotation.SuppressLint; +import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.pm.PackageManager; -import android.graphics.Bitmap; -import android.graphics.Color; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.preference.PreferenceManager; import android.text.TextUtils; -import android.util.AndroidRuntimeException; +import android.util.Pair; import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.EditorInfo; import android.webkit.CookieManager; -import android.webkit.CookieSyncManager; +import android.webkit.URLUtil; +import android.webkit.WebResourceError; import android.webkit.WebResourceRequest; -import android.webkit.WebResourceResponse; import android.webkit.WebView; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.LinearLayout; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; import android.widget.Toast; import com.blikoon.qrcodescanner.QrCodeActivity; -import com.google.android.material.snackbar.Snackbar; +import com.google.android.material.button.MaterialButton; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.reflect.TypeToken; import com.nextcloud.android.common.ui.color.ColorUtil; import com.nextcloud.android.common.ui.theme.utils.ColorRole; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.device.DeviceInfo; import com.nextcloud.client.di.Injectable; +import com.nextcloud.client.network.ClientFactory; import com.nextcloud.client.onboarding.FirstRunActivity; import com.nextcloud.client.onboarding.OnboardingService; import com.nextcloud.client.preferences.AppPreferences; +import com.nextcloud.common.PlainClient; +import com.nextcloud.operations.PostMethod; import com.nextcloud.utils.extensions.BundleExtensionsKt; +import com.nextcloud.utils.mdm.MDMConfig; +import com.owncloud.android.BuildConfig; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.databinding.AccountSetupBinding; import com.owncloud.android.databinding.AccountSetupWebviewBinding; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.lib.common.OwnCloudAccount; -import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.OwnCloudClientFactory; import com.owncloud.android.lib.common.OwnCloudClientManagerFactory; import com.owncloud.android.lib.common.OwnCloudCredentials; import com.owncloud.android.lib.common.OwnCloudCredentialsFactory; import com.owncloud.android.lib.common.UserInfo; +import com.owncloud.android.lib.common.accounts.AccountUtils; import com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException; import com.owncloud.android.lib.common.accounts.AccountUtils.Constants; import com.owncloud.android.lib.common.network.CertificateCombinedException; @@ -77,9 +86,6 @@ import com.owncloud.android.lib.common.operations.RemoteOperation; import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.status.GetCapabilitiesRemoteOperation; -import com.owncloud.android.lib.resources.status.NextcloudVersion; -import com.owncloud.android.lib.resources.status.OCCapability; import com.owncloud.android.lib.resources.status.OwnCloudVersion; import com.owncloud.android.lib.resources.users.GetUserInfoRemoteOperation; import com.owncloud.android.operations.DetectAuthenticationMethodOperation.AuthenticationMethod; @@ -90,6 +96,7 @@ import com.owncloud.android.services.OperationsService; import com.owncloud.android.services.OperationsService.OperationsServiceBinder; import com.owncloud.android.ui.NextcloudWebViewClient; import com.owncloud.android.ui.activity.FileDisplayActivity; +import com.owncloud.android.ui.activity.SettingsActivity; import com.owncloud.android.ui.dialog.IndeterminateProgressDialog; import com.owncloud.android.ui.dialog.SslUntrustedCertDialog; import com.owncloud.android.ui.dialog.SslUntrustedCertDialog.OnSslUntrustedCertListener; @@ -97,34 +104,42 @@ import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.ErrorMessageAdapter; import com.owncloud.android.utils.PermissionUtil; import com.owncloud.android.utils.WebViewUtil; -import com.owncloud.android.utils.theme.CapabilityUtils; import com.owncloud.android.utils.theme.ViewThemeUtils; import java.io.InputStream; import java.net.URLDecoder; +import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Optional; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; -import androidx.annotation.ColorInt; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.ActionBar; -import androidx.core.content.ContextCompat; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; -import de.cotech.hw.fido.WebViewFidoBridge; -import de.cotech.hw.fido.ui.FidoDialogOptions; -import de.cotech.hw.fido2.WebViewWebauthnBridge; -import de.cotech.hw.fido2.ui.WebauthnDialogOptions; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleEventObserver; +import androidx.lifecycle.ProcessLifecycleOwner; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import okhttp3.FormBody; +import okhttp3.RequestBody; import static com.owncloud.android.utils.PermissionUtil.PERMISSIONS_CAMERA; @@ -162,7 +177,9 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity private static final String KEY_USERNAME = "USERNAME"; private static final String KEY_PASSWORD = "PASSWORD"; private static final String KEY_ASYNC_TASK_IN_PROGRESS = "AUTH_IN_PROGRESS"; - public static final String WEB_LOGIN = "/index.php/login/flow"; + + public static final String WEB_LOGIN = "/index.php/login/v2"; + public static final String PROTOCOL_SUFFIX = "://"; public static final String LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":"; public static final String HTTPS_PROTOCOL = "https://"; @@ -170,8 +187,6 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity public static final int NO_ICON = 0; public static final String EMPTY_STRING = ""; - - private static final int REQUEST_CODE_QR_SCAN = 101; public static final int REQUEST_CODE_FIRST_RUN = 102; /// parameters from EXTRAs in starter Intent @@ -194,9 +209,6 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity private GetServerInfoOperation.ServerInfo mServerInfo = new GetServerInfoOperation.ServerInfo(); /// Authentication PRE-Fragment elements - private WebViewFidoBridge webViewFidoU2fBridge; - private WebViewWebauthnBridge webViewWebauthnBridge; - private String mAuthStatusText = EMPTY_STRING; private int mAuthStatusIcon; @@ -218,13 +230,16 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity @Inject PassCodeManager passCodeManager; @Inject ViewThemeUtils.Factory viewThemeUtilsFactory; @Inject ColorUtil colorUtil; + @Inject ClientFactory clientFactory; + private AuthObject authObject = null; + private String fallbackToken; private boolean onlyAdd = false; - @SuppressLint("ResourceAsColor") @ColorInt - private int primaryColor = R.color.primary; - private boolean strictMode = false; + + private final Gson gson = new Gson(); private ViewThemeUtils viewThemeUtils; + private final ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); @VisibleForTesting public AccountSetupBinding getAccountSetupBinding() { @@ -240,9 +255,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewThemeUtils = viewThemeUtilsFactory.withPrimaryAsBackground(); - viewThemeUtils.platform.themeStatusBar(this, ColorRole.PRIMARY); - - WebViewUtil webViewUtil = new WebViewUtil(this); + viewThemeUtils.platform.colorStatusBar(this, getResources().getColor(R.color.primary)); Uri data = getIntent().getData(); boolean directLogin = data != null && data.toString().startsWith(getString(R.string.login_data_own_scheme)); @@ -284,13 +297,24 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity mIsFirstAuthAttempt = savedInstanceState.getBoolean(KEY_AUTH_IS_FIRST_ATTEMPT_TAG); } + boolean webViewLoginMethod = false; String webloginUrl = null; - boolean webViewLoginMethod; - if (getIntent().getBooleanExtra(EXTRA_USE_PROVIDER_AS_WEBLOGIN, false)) { + + if (MainApp.isClientBrandedPlus()) { + String baseUrl = MDMConfig.INSTANCE.getBaseUrl(this); + if (!TextUtils.isEmpty(baseUrl)) { + webloginUrl = baseUrl + WEB_LOGIN; + } + } + + if (!TextUtils.isEmpty(webloginUrl)) { + webViewLoginMethod = true; + } else if (getIntent().getBooleanExtra(EXTRA_USE_PROVIDER_AS_WEBLOGIN, false)) { webViewLoginMethod = true; webloginUrl = getString(R.string.provider_registration_server); - } else { - webViewLoginMethod = !TextUtils.isEmpty(getResources().getString(R.string.webview_login_url)); + } else if (!TextUtils.isEmpty(getResources().getString(R.string.webview_login_url))) { + webViewLoginMethod = true; + webloginUrl = getResources().getString(R.string.webview_login_url); showWebViewLoginUrl = getResources().getBoolean(R.bool.show_server_url_input); } @@ -298,7 +322,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity if (webViewLoginMethod) { accountSetupWebviewBinding = AccountSetupWebviewBinding.inflate(getLayoutInflater()); setContentView(accountSetupWebviewBinding.getRoot()); - initWebViewLogin(webloginUrl, false); + anonymouslyPostLoginRequest(webloginUrl); } else { accountSetupBinding = AccountSetupBinding.inflate(getLayoutInflater()); setContentView(accountSetupBinding.getRoot()); @@ -309,80 +333,256 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity /// initialize block to be moved to single Fragment to check server and get info about it /// initialize block to be moved to single Fragment to retrieve and validate credentials - initAuthorizationPreFragment(savedInstanceState); + if (TextUtils.isEmpty(getString(R.string.enforce_servers))) { + initAuthorizationPreFragment(savedInstanceState); + } else { + showEnforcedServers(); + } + + initServerPreFragment(savedInstanceState); } - initServerPreFragment(savedInstanceState); + ProcessLifecycleOwner.get().getLifecycle().addObserver(lifecycleEventObserver); + } - webViewUtil.checkWebViewVersion(); + private void showEnforcedServers() { + showAuthStatus(); + accountSetupBinding.hostUrlFrame.setVisibility(View.GONE); + accountSetupBinding.hostUrlInputHelperText.setVisibility(View.GONE); + accountSetupBinding.scanQr.setVisibility(View.GONE); + accountSetupBinding.serversSpinner.setVisibility(View.VISIBLE); + + ArrayAdapter adapter = new ArrayAdapter<>(this, R.layout.enforced_servers_spinner); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + + ArrayList servers = new ArrayList<>(); + servers.add(""); + adapter.add(getString(R.string.please_select_a_server)); + + ArrayList t = new Gson().fromJson(getString(R.string.enforce_servers), + new TypeToken>() { + } + .getType()); + + for (EnforcedServer e : t) { + adapter.add(e.getName()); + servers.add(e.getUrl()); + } + + accountSetupBinding.serversSpinner.setAdapter(adapter); + accountSetupBinding.serversSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + String url = servers.get(position); + + if (URLUtil.isValidUrl(url)) { + accountSetupBinding.hostUrlInput.setText(url); + checkOcServer(); + } + } + + @Override + public void onNothingSelected(AdapterView parent) { + // do nothing + } + }); } private void deleteCookies() { try { - CookieSyncManager.createInstance(this); CookieManager.getInstance().removeAllCookies(null); - } catch (AndroidRuntimeException e) { + } catch (Exception e) { Log_OC.e(TAG, e.getMessage()); } } - private static String getWebLoginUserAgent() { - return Build.MANUFACTURER.substring(0, 1).toUpperCase(Locale.getDefault()) + - Build.MANUFACTURER.substring(1).toLowerCase(Locale.getDefault()) + " " + Build.MODEL + " (Android)"; + // region LoginFlow + private final ScheduledExecutorService loginFlowExecutorService = Executors.newSingleThreadScheduledExecutor(); + private boolean isLoginProcessCompleted = false; + private boolean isRedirectedToTheDefaultBrowser = false; + private String baseUrl; + + private void poolLogin() { + loginFlowExecutorService.scheduleWithFixedDelay(() -> { + if (!isLoginProcessCompleted) { + performLoginFlowV2(); + } + }, 0, 30, TimeUnit.SECONDS); } - @SuppressFBWarnings("ANDROID_WEB_VIEW_JAVASCRIPT") - @SuppressLint("SetJavaScriptEnabled") - private void initWebViewLogin(String baseURL, boolean useGenericUserAgent) { - viewThemeUtils.platform.colorCircularProgressBar(accountSetupWebviewBinding.loginWebviewProgressBar, ColorRole.ON_PRIMARY_CONTAINER); - accountSetupWebviewBinding.loginWebview.setVisibility(View.GONE); - new WebViewUtil(this).setProxyKKPlus(accountSetupWebviewBinding.loginWebview); - - accountSetupWebviewBinding.loginWebview.getSettings().setAllowFileAccess(false); - accountSetupWebviewBinding.loginWebview.getSettings().setJavaScriptEnabled(true); - accountSetupWebviewBinding.loginWebview.getSettings().setDomStorageEnabled(true); - - if (useGenericUserAgent) { - accountSetupWebviewBinding.loginWebview.getSettings().setUserAgentString(MainApp.getUserAgent()); - } else { - accountSetupWebviewBinding.loginWebview.getSettings().setUserAgentString(getWebLoginUserAgent()); + /** + * This function facilitates the login process by anonymously posting a login request to a specified URL. + * After posting the request, it retrieves the login URL for completing the login flow. + * The login flow version used is v2. + * + * @param url The URL where the login request is to be anonymously posted. + * This URL should handle the login request and return the login URL. + * It's typically the entry point for the login process. + * Example: "..." + */ + private void anonymouslyPostLoginRequest(String url) { + if (TextUtils.isEmpty(url)) { + DisplayUtils.showSnackMessage(this, R.string.authenticator_activity_empty_base_url); + return; } - accountSetupWebviewBinding.loginWebview.getSettings().setSaveFormData(false); - accountSetupWebviewBinding.loginWebview.getSettings().setSavePassword(false); + baseUrl = url; - FidoDialogOptions.Builder dialogOptionsBuilder = FidoDialogOptions.builder(); - dialogOptionsBuilder.setShowSdkLogo(true); - dialogOptionsBuilder.setTheme(R.style.FidoDialog); - webViewFidoU2fBridge = WebViewFidoBridge.createInstanceForWebView( - this, accountSetupWebviewBinding.loginWebview, dialogOptionsBuilder); + singleThreadExecutor.execute(() -> { + String response = getResponseOfAnonymouslyPostLoginRequest(); + if (TextUtils.isEmpty(response)) { + DisplayUtils.showSnackMessage(AuthenticatorActivity.this, R.string.authenticator_activity_empty_response_message); + return; + } - WebauthnDialogOptions.Builder webauthnOptionsBuilder = WebauthnDialogOptions.builder(); - webauthnOptionsBuilder.setShowSdkLogo(true); - webauthnOptionsBuilder.setAllowSkipPin(true); - webauthnOptionsBuilder.setTheme(R.style.FidoDialog); - webViewWebauthnBridge = WebViewWebauthnBridge.createInstanceForWebView( - this, accountSetupWebviewBinding.loginWebview, webauthnOptionsBuilder); - - Map headers = new HashMap<>(); - headers.put(RemoteOperation.OCS_API_HEADER, RemoteOperation.OCS_API_HEADER_VALUE); - - String url; - if (baseURL != null && !baseURL.isEmpty()) { - url = baseURL; - } else { - url = getResources().getString(R.string.webview_login_url); - } - - new WebViewUtil(this).setProxyKKPlus(accountSetupWebviewBinding.loginWebview); - if (url.startsWith(HTTPS_PROTOCOL)) { - strictMode = true; - } - - accountSetupWebviewBinding.loginWebview.loadUrl(url, headers); - - setClient(); + String loginUrl = extractLoginUrl(response); + runOnUiThread(() -> { + initLoginInfoView(); + launchDefaultWebBrowser(loginUrl); + }); + }); } + private String extractLoginUrl(String response) { + try { + authObject = gson.fromJson(response, AuthObject.class); + if (authObject != null && !TextUtils.isEmpty(authObject.getLogin())) { + return authObject.getLogin(); + } else { + Log_OC.e(TAG, "AuthObject parsing failed or login empty, trying JSONObject fallback"); + } + } catch (Exception e) { + Log_OC.e(TAG, "Error parsing AuthObject: " + e.getMessage(), e); + } + + try { + String fallbackUrl = getLoginFromJsonObject(response); + if (!TextUtils.isEmpty(fallbackUrl)) { + return fallbackUrl; + } else { + Log_OC.e(TAG, "Fallback JSONObject parsing failed or login empty"); + } + } catch (Exception e) { + Log_OC.e(TAG, "Error parsing fallback JSONObject: " + e.getMessage(), e); + } + + Log_OC.e(TAG, "Both AuthObject and fallback parsing failed, returning default login URL"); + DisplayUtils.showSnackMessage(this, R.string.authenticator_activity_login_error); + return getResources().getString(R.string.webview_login_url); + } + + private String getLoginFromJsonObject(String response) { + JsonObject jsonObject = JsonParser.parseString(response).getAsJsonObject(); + fallbackToken = jsonObject.getAsJsonObject("poll").get("token").getAsString(); + return jsonObject.get("login").getAsString(); + } + + private String getResponseOfAnonymouslyPostLoginRequest() { + PostMethod post = new PostMethod(baseUrl, false, new FormBody.Builder().build()); + PlainClient client = clientFactory.createPlainClient(); + post.execute(client); + return post.getResponseBodyAsString(); + } + + private void launchDefaultWebBrowser(String url) { + if (url == null || url.isBlank()) { + DisplayUtils.showSnackMessage(this, R.string.invalid_url); + return; + } + + try { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + PackageManager packageManager = getPackageManager(); + + if (intent.resolveActivity(packageManager) != null) { + startActivity(intent); + } else { + DisplayUtils.showSnackMessage(this, R.string.authenticator_activity_no_web_browser_found); + } + } catch (Exception e) { + Log_OC.e(TAG, "Exception launchDefaultWebBrowser: " + e); + DisplayUtils.showSnackMessage(this, R.string.authenticator_activity_login_error); + } + } + + private Pair extractPollUrlAndToken() { + if (authObject != null) { + final var poll = authObject.getPoll(); + String pollUrl = poll.getEndpoint(); + String token = poll.getToken(); + + if (TextUtils.isEmpty(pollUrl)) { + Log_OC.e(TAG, "auth object poll url is empty."); + } + if (TextUtils.isEmpty(token)) { + Log_OC.e(TAG, "auth object token is empty."); + } + + if (!TextUtils.isEmpty(pollUrl) && !TextUtils.isEmpty(token)) { + return new Pair<>(pollUrl, token); + } + } + + return new Pair<>(baseUrl + "/poll", fallbackToken); + } + + private void performLoginFlowV2() { + final var pollUrlAndToken = extractPollUrlAndToken(); + + RequestBody requestBody = new FormBody.Builder() + .add("token", pollUrlAndToken.second) + .build(); + + PlainClient client = clientFactory.createPlainClient(); + PostMethod post = new PostMethod(pollUrlAndToken.first, false, requestBody); + int status = post.execute(client); + String response = post.getResponseBodyAsString(); + + Log_OC.d(TAG, "performLoginFlowV2 status: " + status); + Log_OC.d(TAG, "performLoginFlowV2 response: " + response); + + if (!response.isEmpty()) { + runOnUiThread(() -> completeLoginFlow(response, status)); + } + } + + private void completeLoginFlow(String response, int status) { + try { + LoginUrlInfo loginUrlInfo = gson.fromJson(response, LoginUrlInfo.class); + if (loginUrlInfo == null) { + Log_OC.e(TAG, "cannot complete login flow loginUrl is null"); + return; + } + isLoginProcessCompleted = loginUrlInfo.isValid(status); + + if (accountSetupBinding != null) { + accountSetupBinding.hostUrlInput.setText(""); + } + + mServerInfo.mBaseUrl = AuthenticatorUrlUtils.INSTANCE.normalizeUrlSuffix(loginUrlInfo.getServer()); + webViewUser = loginUrlInfo.getLoginName(); + webViewPassword = loginUrlInfo.getAppPassword(); + } catch (Exception e) { + Log_OC.d(TAG, "Error completeLoginFlow: " + e); + mServerStatusIcon = R.drawable.ic_alert; + mServerStatusText = getString(R.string.qr_could_not_be_read); + showServerStatus(); + } + + checkOcServer(); + loginFlowExecutorService.shutdown(); + ProcessLifecycleOwner.get().getLifecycle().removeObserver(lifecycleEventObserver); + } + + private final LifecycleEventObserver lifecycleEventObserver = ((lifecycleOwner, event) -> { + if (event == Lifecycle.Event.ON_START && authObject != null && !TextUtils.isEmpty(authObject.getPoll().getToken())) { + Log_OC.d(TAG, "Start poolLogin"); + poolLogin(); + } + }); + // endregion + @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (accountSetupWebviewBinding != null && event.getAction() == KeyEvent.ACTION_DOWN && @@ -400,29 +600,12 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity private void setClient() { accountSetupWebviewBinding.loginWebview.setWebViewClient(new NextcloudWebViewClient(getSupportFragmentManager()) { @Override - public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { - webViewFidoU2fBridge.delegateShouldInterceptRequest(view, request); - webViewWebauthnBridge.delegateShouldInterceptRequest(view, request); - return super.shouldInterceptRequest(view, request); - } - - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - super.onPageStarted(view, url, favicon); - webViewFidoU2fBridge.delegateOnPageStarted(view, url, favicon); - webViewWebauthnBridge.delegateOnPageStarted(view, url, favicon); - } - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + String url = request.getUrl().toString(); if (url.startsWith(getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "login/")) { parseAndLoginFromWebView(url); return true; } - if (strictMode && url.startsWith(HTTP_PROTOCOL)) { - Snackbar.make(view, R.string.strict_mode, Snackbar.LENGTH_LONG).show(); - return true; - } return false; } @@ -432,18 +615,10 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity accountSetupWebviewBinding.loginWebviewProgressBar.setVisibility(View.GONE); accountSetupWebviewBinding.loginWebview.setVisibility(View.VISIBLE); - - if (mServerInfo.mVersion != null && mServerInfo.mVersion.isOlderThan(NextcloudVersion.nextcloud_25)) { - viewThemeUtils.platform.colorStatusBar(AuthenticatorActivity.this, primaryColor); - getWindow().setNavigationBarColor(primaryColor); - } else { - viewThemeUtils.platform.resetStatusBar(AuthenticatorActivity.this); - getWindow().setNavigationBarColor(ContextCompat.getColor(AuthenticatorActivity.this, R.color.bg_default)); - } } @Override - public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { accountSetupWebviewBinding.loginWebviewProgressBar.setVisibility(View.GONE); accountSetupWebviewBinding.loginWebview.setVisibility(View.VISIBLE); @@ -465,9 +640,9 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity if (accountSetupBinding != null) { accountSetupBinding.hostUrlInput.setText(""); } - mServerInfo.mBaseUrl = AuthenticatorUrlUtils.INSTANCE.normalizeUrlSuffix(loginUrlInfo.serverAddress); - webViewUser = loginUrlInfo.username; - webViewPassword = loginUrlInfo.password; + mServerInfo.mBaseUrl = AuthenticatorUrlUtils.INSTANCE.normalizeUrlSuffix(loginUrlInfo.getServer()); + webViewUser = loginUrlInfo.getLoginName(); + webViewPassword = loginUrlInfo.getAppPassword(); } catch (Exception e) { mServerStatusIcon = R.drawable.ic_alert; mServerStatusText = getString(R.string.qr_could_not_be_read); @@ -500,18 +675,18 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity throw new IllegalArgumentException("Illegal number of login URL elements detected: " + values.length); } - LoginUrlInfo loginUrlInfo = new LoginUrlInfo(); + LoginUrlInfo loginUrlInfo = new LoginUrlInfo("", "", ""); for (String value : values) { if (value.startsWith("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { - loginUrlInfo.username = URLDecoder.decode( - value.substring(("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())); + loginUrlInfo.setLoginName(URLDecoder.decode( + value.substring(("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length()))); } else if (value.startsWith("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { - loginUrlInfo.password = URLDecoder.decode( - value.substring(("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())); + loginUrlInfo.setAppPassword(URLDecoder.decode( + value.substring(("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length()))); } else if (value.startsWith("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { - loginUrlInfo.serverAddress = URLDecoder.decode( - value.substring(("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())); + loginUrlInfo.setServer(URLDecoder.decode( + value.substring(("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length()))); } } @@ -539,6 +714,22 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity } else { accountSetupBinding.scanQr.setVisibility(View.GONE); } + + addDebugLogin(); + } + + private void addDebugLogin() { + if (BuildConfig.DEBUG) { + try { + accountSetupBinding.thumbnail.setOnLongClickListener(v -> { + final String dataString = BuildConfig.NC_TEST_SERVER_DATA_STRING; + parseAndLoginFromWebView(dataString); + return false; + }); + } catch (Throwable t) { + Log_OC.w(TAG, "Test server data string not available in this build"); + } + } } /** @@ -665,9 +856,8 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity passCodeManager.onActivityResumed(this); Uri data = intent.getData(); - if (data != null && data.toString().startsWith(getString(R.string.login_data_own_scheme))) { - if (!getResources().getBoolean(R.bool.multiaccount_support) && + if (!MDMConfig.INSTANCE.multiAccountSupport(this) && accountManager.getAccounts().length == 1) { Toast.makeText(this, R.string.no_mutliple_accounts_allowed, Toast.LENGTH_LONG).show(); finish(); @@ -680,10 +870,36 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity if (intent.getBooleanExtra(EXTRA_USE_PROVIDER_AS_WEBLOGIN, false)) { accountSetupWebviewBinding = AccountSetupWebviewBinding.inflate(getLayoutInflater()); setContentView(accountSetupWebviewBinding.getRoot()); - initWebViewLogin(getString(R.string.provider_registration_server), true); + initSimpleSignupLogin(); } } + @SuppressFBWarnings("ANDROID_WEB_VIEW_JAVASCRIPT") + @SuppressLint("SetJavaScriptEnabled") + private void initSimpleSignupLogin() { + viewThemeUtils.platform.colorCircularProgressBar(accountSetupWebviewBinding.loginWebviewProgressBar, ColorRole.ON_PRIMARY_CONTAINER); + accountSetupWebviewBinding.loginWebview.setVisibility(View.GONE); + new WebViewUtil().setProxyKKPlus(accountSetupWebviewBinding.loginWebview); + + accountSetupWebviewBinding.loginWebview.getSettings().setAllowFileAccess(false); + accountSetupWebviewBinding.loginWebview.getSettings().setJavaScriptEnabled(true); + accountSetupWebviewBinding.loginWebview.getSettings().setDomStorageEnabled(true); + + accountSetupWebviewBinding.loginWebview.getSettings().setUserAgentString(MainApp.getUserAgent()); + accountSetupWebviewBinding.loginWebview.getSettings().setSaveFormData(false); + accountSetupWebviewBinding.loginWebview.getSettings().setSavePassword(false); + + Map headers = new HashMap<>(); + headers.put(RemoteOperation.OCS_API_HEADER, RemoteOperation.OCS_API_HEADER_VALUE); + + new WebViewUtil().setProxyKKPlus(accountSetupWebviewBinding.loginWebview); + + accountSetupWebviewBinding.loginWebview.loadUrl(getString(R.string.provider_registration_server), headers); + accountSetupWebviewBinding.loginFlowV2.loginFlowInfoV2.setVisibility(View.GONE); + + setClient(); + } + private boolean checkIfViaSSO(Intent intent) { Bundle extras = intent.getExtras(); if (extras == null) { @@ -734,6 +950,8 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity mOperationsServiceBinder = null; } + Log_OC.d(TAG, "AuthenticatorActivity onDestroy called"); + singleThreadExecutor.shutdown(); super.onDestroy(); } @@ -752,7 +970,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity mServerInfo = new GetServerInfoOperation.ServerInfo(); - if (uri.length() != 0) { + if (!uri.isEmpty()) { if (accountSetupBinding != null) { uri = AuthenticatorUrlUtils.INSTANCE.stripIndexPhpOrAppsFiles(uri); accountSetupBinding.hostUrlInput.setText(uri); @@ -884,38 +1102,21 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity // 4. we got the authentication method required by the server mServerInfo = (GetServerInfoOperation.ServerInfo) (result.getData().get(0)); - // show outdated warning - if (CapabilityUtils.checkOutdatedWarning(getResources(), - mServerInfo.mVersion, - mServerInfo.hasExtendedSupport)) { - DisplayUtils.showServerOutdatedSnackbar(this, Snackbar.LENGTH_INDEFINITE); - } - if (webViewUser != null && !webViewUser.isEmpty() && webViewPassword != null && !webViewPassword.isEmpty()) { checkBasicAuthorization(webViewUser, webViewPassword); } else { - new Thread(() -> { - OwnCloudClient client = OwnCloudClientFactory.createOwnCloudClient(Uri.parse(mServerInfo.mBaseUrl), - this, - true); - RemoteOperationResult remoteOperationResult = new GetCapabilitiesRemoteOperation().execute(client); - - if (remoteOperationResult.isSuccess() && - remoteOperationResult.getData() != null && - remoteOperationResult.getData().size() > 0) { - OCCapability capability = (OCCapability) remoteOperationResult.getData().get(0); - try { - primaryColor = Color.parseColor(capability.getServerColor()); - } catch (Exception e) { - // falls back to primary color - } - } - }).start(); - accountSetupWebviewBinding = AccountSetupWebviewBinding.inflate(getLayoutInflater()); setContentView(accountSetupWebviewBinding.getRoot()); - initWebViewLogin(mServerInfo.mBaseUrl + WEB_LOGIN, false); + + if (!isLoginProcessCompleted) { + if (!isRedirectedToTheDefaultBrowser) { + anonymouslyPostLoginRequest(mServerInfo.mBaseUrl + WEB_LOGIN); + isRedirectedToTheDefaultBrowser = true; + } else { + initLoginInfoView(); + } + } } } else { updateServerStatusIconAndText(result); @@ -928,6 +1129,31 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity } } + // region LoginInfoView + private void initLoginInfoView() { + LinearLayout loginFlowLayout = accountSetupWebviewBinding.loginFlowV2.getRoot(); + MaterialButton cancelButton = accountSetupWebviewBinding.loginFlowV2.cancelButton; + loginFlowLayout.setVisibility(View.VISIBLE); + + // add margin bottom to prevent overlapping with system bars + ViewCompat.setOnApplyWindowInsetsListener(loginFlowLayout, (view, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + view.setPadding( + view.getPaddingLeft(), + view.getPaddingTop(), + view.getPaddingRight(), + systemBars.bottom); + return insets; + }); + + cancelButton.setOnClickListener(v -> { + loginFlowExecutorService.shutdown(); + ProcessLifecycleOwner.get().getLifecycle().removeObserver(lifecycleEventObserver); + recreate(); + }); + } + // endregion + /** * Chooses the right icon and text to show to the user for the received operation result. * @@ -1139,10 +1365,6 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity accountManager.setCurrentOwnCloudAccount(mAccount.name); getUserCapabilitiesAndFinish(); } else { - // init webView again - if (accountSetupWebviewBinding != null) { - accountSetupWebviewBinding.loginWebview.setVisibility(View.GONE); - } accountSetupBinding = AccountSetupBinding.inflate(getLayoutInflater()); setContentView(accountSetupBinding.getRoot()); initOverallUi(); @@ -1169,12 +1391,9 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity } else { // authorization fail due to client side - probably wrong credentials if (accountSetupWebviewBinding != null) { - initWebViewLogin(mServerInfo.mBaseUrl + WEB_LOGIN, false); - DisplayUtils.showSnackMessage(this, - accountSetupWebviewBinding.loginWebview, R.string.auth_access_failed, - result.getLogMessage()); + anonymouslyPostLoginRequest(mServerInfo.mBaseUrl + WEB_LOGIN); } else { - DisplayUtils.showSnackMessage(this, R.string.auth_access_failed, result.getLogMessage()); + DisplayUtils.showSnackMessage(this, R.string.auth_access_failed, result.getLogMessage(this)); // init webView again updateAuthStatusIconAndText(result); @@ -1190,14 +1409,19 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity } private void endSuccess() { - if (onlyAdd) { - finish(); - } else { - Intent i = new Intent(this, FileDisplayActivity.class); - i.setAction(FileDisplayActivity.RESTART); - i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(i); + if (!onlyAdd) { + if (MDMConfig.INSTANCE.enforceProtection(this) && Objects.equals(preferences.getLockPreference(), SettingsActivity.LOCK_NONE)) { + Intent i = new Intent(this, SettingsActivity.class); + startActivity(i); + } else { + Intent i = new Intent(this, FileDisplayActivity.class); + i.setAction(FileDisplayActivity.RESTART); + i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(i); + } } + + finish(); } private void getUserCapabilitiesAndFinish() { @@ -1270,7 +1494,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity // can be anything: email, name, name with whitespaces String loginName = webViewUser; - String accountName = com.owncloud.android.lib.common.accounts.AccountUtils.buildAccountName(uri, loginName); + String accountName = AccountUtils.buildAccountName(uri, loginName); Account newAccount = new Account(accountName, accountType); if (accountManager.exists(newAccount)) { // fail - not a new account, but an existing one; disallow @@ -1340,14 +1564,42 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity private void startQRScanner() { Intent intent = new Intent(this, QrCodeActivity.class); - startActivityForResult(intent, REQUEST_CODE_QR_SCAN); + qrScanResultLauncher.launch(intent); } + private final ActivityResultLauncher qrScanResultLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == Activity.RESULT_OK) { + Intent data = result.getData(); + + if (data == null) { + return; + } + + String resultData = data.getStringExtra("com.blikoon.qrcodescanner.got_qr_scan_relult"); + + if (resultData == null || !resultData.startsWith(getString(R.string.login_data_own_scheme))) { + mServerStatusIcon = R.drawable.ic_alert; + mServerStatusText = "QR Code could not be read!"; + showServerStatus(); + return; + } + + if (!MDMConfig.INSTANCE.multiAccountSupport(this) && + accountManager.getAccounts().length == 1) { + Toast.makeText(this, R.string.no_mutliple_accounts_allowed, Toast.LENGTH_LONG).show(); + } else { + parseAndLoginFromWebView(resultData); + } + } + }); + @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (requestCode == PermissionUtil.PERMISSIONS_CAMERA) {// If request is cancelled, result arrays are empty. + if (requestCode == PERMISSIONS_CAMERA) {// If request is cancelled, result arrays are empty. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // permission was granted startQRScanner(); @@ -1362,7 +1614,9 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity * server. */ private void showServerStatus() { - if (accountSetupBinding == null) return; + if (accountSetupBinding == null) { + return; + } if (mServerStatusIcon == NO_ICON && EMPTY_STRING.equals(mServerStatusText)) { accountSetupBinding.serverStatusText.setVisibility(View.INVISIBLE); @@ -1426,12 +1680,9 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity } } - private void dismissWaitingDialog() { Fragment frag = getSupportFragmentManager().findFragmentByTag(WAIT_DIALOG_TAG); - if (frag instanceof DialogFragment) { - DialogFragment dialog = (DialogFragment) frag; - + if (frag instanceof DialogFragment dialog) { try { dialog.dismiss(); } catch (IllegalStateException e) { @@ -1459,9 +1710,9 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity String prefix = getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "login/"; LoginUrlInfo loginUrlInfo = parseLoginDataUrl(prefix, data.toString()); - mServerInfo.mBaseUrl = AuthenticatorUrlUtils.INSTANCE.normalizeUrlSuffix(loginUrlInfo.serverAddress); - webViewUser = loginUrlInfo.username; - webViewPassword = loginUrlInfo.password; + mServerInfo.mBaseUrl = AuthenticatorUrlUtils.INSTANCE.normalizeUrlSuffix(loginUrlInfo.getServer()); + webViewUser = loginUrlInfo.getLoginName(); + webViewPassword = loginUrlInfo.getAppPassword(); doOnResumeAndBound(); checkOcServer(); } catch (Exception e) { @@ -1486,31 +1737,6 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity } } - @Override - protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_CODE_QR_SCAN) { - if (data == null) { - return; - } - - String result = data.getStringExtra("com.blikoon.qrcodescanner.got_qr_scan_relult"); - - if (result == null || !result.startsWith(getString(R.string.login_data_own_scheme))) { - mServerStatusIcon = R.drawable.ic_alert; - mServerStatusText = "QR Code could not be read!"; - showServerStatus(); - return; - } - - if (!getResources().getBoolean(R.bool.multiaccount_support) && - accountManager.getAccounts().length == 1) { - Toast.makeText(this, R.string.no_mutliple_accounts_allowed, Toast.LENGTH_LONG).show(); - } else { - parseAndLoginFromWebView(result); - } - } - } /** * Called from SslValidatorDialog when a new server certificate was correctly saved. diff --git a/app/src/main/java/com/owncloud/android/authentication/AuthenticatorAsyncTask.kt b/app/src/main/java/com/owncloud/android/authentication/AuthenticatorAsyncTask.kt index 29d6ce0..0a66a90 100644 --- a/app/src/main/java/com/owncloud/android/authentication/AuthenticatorAsyncTask.kt +++ b/app/src/main/java/com/owncloud/android/authentication/AuthenticatorAsyncTask.kt @@ -1,11 +1,11 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2021 Tobias Kaminsky * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2013-2015 María Asensio Valverde - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ @file:Suppress("DEPRECATION") @@ -13,8 +13,8 @@ package com.owncloud.android.authentication import android.app.Activity import android.content.Context -import android.net.Uri import android.os.AsyncTask +import androidx.core.net.toUri import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.common.OwnCloudClientFactory import com.owncloud.android.lib.common.OwnCloudCredentials @@ -46,7 +46,7 @@ class AuthenticatorAsyncTask(activity: Activity) : AsyncTask + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2017 Andy Scherzinger - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.authentication @@ -14,7 +14,7 @@ import java.net.URI */ object AuthenticatorUrlUtils { - const val WEBDAV_PATH_4_0_AND_LATER = "/remote.php/webdav" + private const val REMOTE_PHP_PATH = "/remote.php/dav" fun normalizeUrlSuffix(url: String): String { var normalizedUrl = url @@ -29,19 +29,17 @@ object AuthenticatorUrlUtils { while (trimmedUrl.endsWith("/")) { trimmedUrl = trimmedUrl.substring(0, url.length - 1) } - val pos = trimmedUrl.lastIndexOf(WEBDAV_PATH_4_0_AND_LATER) + val pos = trimmedUrl.lastIndexOf(REMOTE_PHP_PATH) if (pos >= 0) { trimmedUrl = trimmedUrl.substring(0, pos) } return trimmedUrl } - private fun trimUrlWebdav(url: String): String { - return if (url.lowercase().endsWith(WEBDAV_PATH_4_0_AND_LATER)) { - url.substring(0, url.length - WEBDAV_PATH_4_0_AND_LATER.length) - } else { - url - } + private fun trimUrlWebdav(url: String): String = if (url.lowercase().endsWith(REMOTE_PHP_PATH)) { + url.substring(0, url.length - REMOTE_PHP_PATH.length) + } else { + url } fun stripIndexPhpOrAppsFiles(url: String): String { @@ -54,13 +52,11 @@ object AuthenticatorUrlUtils { return strippedUrl } - fun normalizeScheme(url: String): String { - return if (url.matches("[a-zA-Z][a-zA-Z0-9+.-]+://.+".toRegex())) { - val uri = URI.create(url) - val lcScheme = uri.scheme.lowercase() - String.format("%s:%s", lcScheme, uri.rawSchemeSpecificPart) - } else { - url - } + fun normalizeScheme(url: String): String = if (url.matches("[a-zA-Z][a-zA-Z0-9+.-]+://.+".toRegex())) { + val uri = URI.create(url) + val lcScheme = uri.scheme.lowercase() + String.format("%s:%s", lcScheme, uri.rawSchemeSpecificPart) + } else { + url } } diff --git a/app/src/main/java/com/owncloud/android/authentication/DeepLinkLoginActivity.kt b/app/src/main/java/com/owncloud/android/authentication/DeepLinkLoginActivity.kt index 24d9faf..610af50 100644 --- a/app/src/main/java/com/owncloud/android/authentication/DeepLinkLoginActivity.kt +++ b/app/src/main/java/com/owncloud/android/authentication/DeepLinkLoginActivity.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2018-2022 Tobias Kaminsky - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.authentication @@ -11,16 +11,16 @@ import android.os.Bundle import android.widget.TextView import android.widget.Toast import com.nextcloud.client.di.Injectable +import com.nextcloud.utils.mdm.MDMConfig import com.owncloud.android.R -class DeepLinkLoginActivity : AuthenticatorActivity(), Injectable { +class DeepLinkLoginActivity : + AuthenticatorActivity(), + Injectable { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - if (!resources.getBoolean(R.bool.multiaccount_support) && - accountManager.accounts.size == 1 - ) { + if (!MDMConfig.multiAccountSupport(this) && accountManager.accounts.size == 1) { Toast.makeText(this, R.string.no_mutliple_accounts_allowed, Toast.LENGTH_LONG).show() return } @@ -33,8 +33,9 @@ class DeepLinkLoginActivity : AuthenticatorActivity(), Injectable { val loginUrlInfo = parseLoginDataUrl(prefix, it.toString()) val loginText = findViewById(R.id.loginInfo) loginText.text = String.format( - getString(R.string.direct_login_text), loginUrlInfo.username, - loginUrlInfo.serverAddress + getString(R.string.direct_login_text), + loginUrlInfo.loginName, + loginUrlInfo.server ) } catch (e: IllegalArgumentException) { Toast.makeText(this, R.string.direct_login_failed, Toast.LENGTH_LONG).show() diff --git a/app/src/main/java/com/owncloud/android/authentication/EnforcedServer.kt b/app/src/main/java/com/owncloud/android/authentication/EnforcedServer.kt new file mode 100644 index 0000000..b9483d2 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/authentication/EnforcedServer.kt @@ -0,0 +1,10 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Tobias Kaminsky + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.authentication + +data class EnforcedServer(val name: String, val url: String) diff --git a/app/src/main/java/com/owncloud/android/authentication/LoginUrlInfo.java b/app/src/main/java/com/owncloud/android/authentication/LoginUrlInfo.java deleted file mode 100644 index 026b556..0000000 --- a/app/src/main/java/com/owncloud/android/authentication/LoginUrlInfo.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2016 Andy Scherzinger - * SPDX-FileCopyrightText: 2016 Nextcloud - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.owncloud.android.authentication; - -/** - * Data object holding the login url fields. - */ -public class LoginUrlInfo { - String serverAddress; - String username; - String password; -} diff --git a/app/src/main/java/com/owncloud/android/authentication/LoginUrlInfo.kt b/app/src/main/java/com/owncloud/android/authentication/LoginUrlInfo.kt new file mode 100644 index 0000000..a752d40 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/authentication/LoginUrlInfo.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-FileCopyrightText: 2016 Andy Scherzinger + * SPDX-FileCopyrightText: 2016 Nextcloud + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.owncloud.android.authentication + +import com.nextcloud.model.HTTPStatusCodes + +data class LoginUrlInfo(var server: String, var loginName: String, var appPassword: String) { + fun isValid(status: Int): Boolean = ( + status == HTTPStatusCodes.SUCCESS.code && + server.isNotEmpty() && + loginName.isNotEmpty() && + appPassword.isNotEmpty() + ) +} diff --git a/app/src/main/java/com/owncloud/android/authentication/PassCodeManager.kt b/app/src/main/java/com/owncloud/android/authentication/PassCodeManager.kt index 2564806..4694629 100644 --- a/app/src/main/java/com/owncloud/android/authentication/PassCodeManager.kt +++ b/app/src/main/java/com/owncloud/android/authentication/PassCodeManager.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2022-2023 Álvaro Brey - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.authentication @@ -37,19 +37,28 @@ class PassCodeManager(private val preferences: AppPreferences, private val clock * the pass code being requested on screen rotations. */ private const val PASS_CODE_TIMEOUT = 5000 + + fun setSecureFlag(activity: Activity, isSet: Boolean) { + activity.window?.let { window -> + if (isSet) { + println("flag added") + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + } else { + println("flag cleared") + window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + } + } } var canAskPin = true private var askPinWhenDeviceLocked = false - private fun isExemptActivity(activity: Activity): Boolean { - return exemptOfPasscodeActivities.contains(activity.javaClass) - } + private fun isExemptActivity(activity: Activity): Boolean = exemptOfPasscodeActivities.contains(activity.javaClass) fun onActivityResumed(activity: Activity): Boolean { var askedForPin = false val timestamp = preferences.lockTimestamp - setSecureFlag(activity) if (!isExemptActivity(activity)) { val passcodeRequested = passCodeShouldBeRequested(timestamp) @@ -76,16 +85,6 @@ class PassCodeManager(private val preferences: AppPreferences, private val clock return askedForPin } - private fun setSecureFlag(activity: Activity) { - activity.window?.let { window -> - if (isPassCodeEnabled() || deviceCredentialsAreEnabled(activity)) { - window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) - } else { - window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) - } - } - } - private fun requestPasscode(activity: Activity) { val i = Intent(MainApp.getAppContext(), PassCodeActivity::class.java).apply { action = PassCodeActivity.ACTION_CHECK @@ -116,28 +115,21 @@ class PassCodeManager(private val preferences: AppPreferences, private val clock /** * `true` if the time elapsed since last unlock is longer than [PASS_CODE_TIMEOUT] and no activities are visible */ - private fun shouldBeLocked(timestamp: Long): Boolean { - return (abs(clock.millisSinceBoot - timestamp) > PASS_CODE_TIMEOUT && canAskPin) || askPinWhenDeviceLocked - } + private fun shouldBeLocked(timestamp: Long): Boolean = + (abs(clock.millisSinceBoot - timestamp) > PASS_CODE_TIMEOUT && canAskPin) || askPinWhenDeviceLocked @VisibleForTesting - fun passCodeShouldBeRequested(timestamp: Long): Boolean { - return shouldBeLocked(timestamp) && isPassCodeEnabled() - } + fun passCodeShouldBeRequested(timestamp: Long): Boolean = shouldBeLocked(timestamp) && isPassCodeEnabled() private fun isPassCodeEnabled(): Boolean = SettingsActivity.LOCK_PASSCODE == preferences.lockPreference - private fun deviceCredentialsShouldBeRequested(timestamp: Long, activity: Activity): Boolean { - return shouldBeLocked(timestamp) && deviceCredentialsAreEnabled(activity) - } + private fun deviceCredentialsShouldBeRequested(timestamp: Long, activity: Activity): Boolean = + shouldBeLocked(timestamp) && deviceCredentialsAreEnabled(activity) - private fun deviceCredentialsAreEnabled(activity: Activity): Boolean { - return SettingsActivity.LOCK_DEVICE_CREDENTIALS == preferences.lockPreference || - (preferences.isFingerprintUnlockEnabled && DeviceCredentialUtils.areCredentialsAvailable(activity)) - } + private fun deviceCredentialsAreEnabled(activity: Activity): Boolean = + (preferences.lockPreference == SettingsActivity.LOCK_DEVICE_CREDENTIALS) && + DeviceCredentialUtils.areCredentialsAvailable(activity) - private fun getActivityRootView(activity: Activity): View? { - return activity.window?.findViewById(android.R.id.content) - ?: activity.window?.decorView?.findViewById(android.R.id.content) - } + private fun getActivityRootView(activity: Activity): View? = activity.window?.findViewById(android.R.id.content) + ?: activity.window?.decorView?.findViewById(android.R.id.content) } diff --git a/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.kt b/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.kt index 466fbb7..c7b40aa 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProvider.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel diff --git a/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProviderImpl.java b/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProviderImpl.java index 950eedf..1acd7f3 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProviderImpl.java +++ b/app/src/main/java/com/owncloud/android/datamodel/ArbitraryDataProviderImpl.java @@ -5,7 +5,7 @@ * Copyright (C) 2017 Mario Danic * Copyright (C) 2017 Nextcloud. * - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel; diff --git a/app/src/main/java/com/owncloud/android/datamodel/ContentResolverHelper.kt b/app/src/main/java/com/owncloud/android/datamodel/ContentResolverHelper.kt index 9be600d..4f2f604 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/ContentResolverHelper.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/ContentResolverHelper.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2021 Álvaro Brey * SPDX-FileCopyrightText: 2021 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel @@ -83,22 +83,20 @@ object ContentResolverHelper { sortColumn: String?, sortDirection: String?, limit: Int? - ): Bundle { - return Bundle().apply { - if (selection != null) { - putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection) - } - if (sortColumn != null) { - putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, arrayOf(sortColumn)) - val direction = when (sortDirection) { - SORT_DIRECTION_ASCENDING -> ContentResolver.QUERY_SORT_DIRECTION_ASCENDING - else -> ContentResolver.QUERY_SORT_DIRECTION_DESCENDING - } - putInt(ContentResolver.QUERY_ARG_SORT_DIRECTION, direction) - } - if (limit != null) { - putInt(ContentResolver.QUERY_ARG_LIMIT, limit) + ): Bundle = Bundle().apply { + if (selection != null) { + putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection) + } + if (sortColumn != null) { + putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, arrayOf(sortColumn)) + val direction = when (sortDirection) { + SORT_DIRECTION_ASCENDING -> ContentResolver.QUERY_SORT_DIRECTION_ASCENDING + else -> ContentResolver.QUERY_SORT_DIRECTION_DESCENDING } + putInt(ContentResolver.QUERY_ARG_SORT_DIRECTION, direction) + } + if (limit != null) { + putInt(ContentResolver.QUERY_ARG_LIMIT, limit) } } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/DecryptedFolderMetadataOld.java b/app/src/main/java/com/owncloud/android/datamodel/DecryptedFolderMetadataOld.java index f1977e1..98712fb 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/DecryptedFolderMetadataOld.java +++ b/app/src/main/java/com/owncloud/android/datamodel/DecryptedFolderMetadataOld.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2017 Tobias Kaminsky * SPDX-FileCopyrightText: 2017 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel; diff --git a/app/src/main/java/com/owncloud/android/datamodel/DecryptedPushMessage.kt b/app/src/main/java/com/owncloud/android/datamodel/DecryptedPushMessage.kt index c4fdb52..301a149 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/DecryptedPushMessage.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/DecryptedPushMessage.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2022 Unpublished - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel diff --git a/app/src/main/java/com/owncloud/android/datamodel/EncryptedFiledrop.kt b/app/src/main/java/com/owncloud/android/datamodel/EncryptedFiledrop.kt index bfab7f8..9d035aa 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/EncryptedFiledrop.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/EncryptedFiledrop.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel diff --git a/app/src/main/java/com/owncloud/android/datamodel/ExternalLinksProvider.java b/app/src/main/java/com/owncloud/android/datamodel/ExternalLinksProvider.java index 79f13db..a50be70 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/ExternalLinksProvider.java +++ b/app/src/main/java/com/owncloud/android/datamodel/ExternalLinksProvider.java @@ -4,7 +4,7 @@ * Copyright (C) 2017 Tobias Kaminsky * Copyright (C) 2017 Nextcloud. * - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel; @@ -43,31 +43,26 @@ public class ExternalLinksProvider { * Stores an external link in database. * * @param externalLink object to store - * @return external link id, -1 if the insert process fails. */ - public long storeExternalLink(ExternalLink externalLink) { + public void storeExternalLink(ExternalLink externalLink) { Log_OC.v(TAG, "Adding " + externalLink.getName()); ContentValues cv = createContentValuesFromExternalLink(externalLink); Uri result = mContentResolver.insert(ProviderMeta.ProviderTableMeta.CONTENT_URI_EXTERNAL_LINKS, cv); - if (result != null) { - return Long.parseLong(result.getPathSegments().get(1)); - } else { + if (result == null) { Log_OC.e(TAG, "Failed to insert item " + externalLink.getName() + " into external link db."); - return -1; } } /** * Delete all external links from the db - * @return numbers of rows deleted */ - public int deleteAllExternalLinks() { - return mContentResolver.delete(ProviderMeta.ProviderTableMeta.CONTENT_URI_EXTERNAL_LINKS, - null, - null); + public void deleteAllExternalLinks() { + mContentResolver.delete(ProviderMeta.ProviderTableMeta.CONTENT_URI_EXTERNAL_LINKS, + null, + null); } /** @@ -138,22 +133,13 @@ public class ExternalLinksProvider { ProviderMeta.ProviderTableMeta.EXTERNAL_LINKS_ICON_URL)); String language = cursor.getString(cursor.getColumnIndexOrThrow( ProviderMeta.ProviderTableMeta.EXTERNAL_LINKS_LANGUAGE)); - ExternalLinkType type; - switch (cursor.getString(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.EXTERNAL_LINKS_TYPE))) { - case "link": - type = ExternalLinkType.LINK; - break; - case "settings": - type = ExternalLinkType.SETTINGS; - break; - case "quota": - type = ExternalLinkType.QUOTA; - break; - default: - type = ExternalLinkType.UNKNOWN; - break; - } + ExternalLinkType type = switch (cursor.getString(cursor.getColumnIndexOrThrow( + ProviderMeta.ProviderTableMeta.EXTERNAL_LINKS_TYPE))) { + case "link" -> ExternalLinkType.LINK; + case "settings" -> ExternalLinkType.SETTINGS; + case "quota" -> ExternalLinkType.QUOTA; + default -> ExternalLinkType.UNKNOWN; + }; String name = cursor.getString(cursor.getColumnIndexOrThrow(ProviderMeta.ProviderTableMeta.EXTERNAL_LINKS_NAME)); String url = cursor.getString(cursor.getColumnIndexOrThrow(ProviderMeta.ProviderTableMeta.EXTERNAL_LINKS_URL)); boolean redirect = cursor.getInt( diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index f930873..517f402 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -1,18 +1,19 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk - * SPDX-FileCopyrightText: 2022 TSI-mc + * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2022-2025 TSI-mc * SPDX-FileCopyrightText: 2021 Chris Narkiewicz * SPDX-FileCopyrightText: 2018-2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2018 Andy Scherzinger * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2012 David A. Velasco * SPDX-FileCopyrightText: 2011 Bartosz Przybylski - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.datamodel; +import android.annotation.SuppressLint; import android.content.ContentProviderClient; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; @@ -31,10 +32,22 @@ import android.text.TextUtils; import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; +import com.nextcloud.android.lib.resources.files.FileDownloadLimit; import com.nextcloud.client.account.User; import com.nextcloud.client.database.NextcloudDatabase; import com.nextcloud.client.database.dao.FileDao; +import com.nextcloud.client.database.dao.OfflineOperationDao; +import com.nextcloud.client.database.dao.RecommendedFileDao; import com.nextcloud.client.database.entity.FileEntity; +import com.nextcloud.client.database.entity.OfflineOperationEntity; +import com.nextcloud.client.jobs.offlineOperations.repository.OfflineOperationsRepository; +import com.nextcloud.client.jobs.offlineOperations.repository.OfflineOperationsRepositoryType; +import com.nextcloud.model.OCFileFilterType; +import com.nextcloud.model.OfflineOperationRawType; +import com.nextcloud.model.OfflineOperationType; +import com.nextcloud.model.ShareeEntry; +import com.nextcloud.utils.date.DateFormatPattern; +import com.nextcloud.utils.extensions.DateExtensionsKt; import com.owncloud.android.MainApp; import com.owncloud.android.db.ProviderMeta.ProviderTableMeta; import com.owncloud.android.lib.common.network.WebdavEntry; @@ -45,12 +58,14 @@ import com.owncloud.android.lib.resources.files.model.FileLockType; import com.owncloud.android.lib.resources.files.model.GeoLocation; import com.owncloud.android.lib.resources.files.model.ImageDimension; import com.owncloud.android.lib.resources.files.model.RemoteFile; +import com.owncloud.android.lib.resources.files.model.ServerFileInterface; import com.owncloud.android.lib.resources.shares.OCShare; import com.owncloud.android.lib.resources.shares.ShareType; import com.owncloud.android.lib.resources.shares.ShareeUser; import com.owncloud.android.lib.resources.status.CapabilityBooleanType; import com.owncloud.android.lib.resources.status.E2EVersion; import com.owncloud.android.lib.resources.status.OCCapability; +import com.owncloud.android.lib.resources.tags.Tag; import com.owncloud.android.operations.RemoteOperationFailedException; import com.owncloud.android.utils.FileStorageUtils; import com.owncloud.android.utils.MimeType; @@ -64,6 +79,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -76,6 +92,7 @@ import androidx.annotation.VisibleForTesting; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import kotlin.Pair; +@SuppressFBWarnings("CE") public class FileDataStorageManager { private static final String TAG = FileDataStorageManager.class.getSimpleName(); @@ -92,19 +109,25 @@ public class FileDataStorageManager { private final ContentProviderClient contentProviderClient; private final User user; - private final FileDao fileDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).fileDao(); + public final RecommendedFileDao recommendedFileDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).recommendedFileDao(); + public final OfflineOperationDao offlineOperationDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).offlineOperationDao(); + public final FileDao fileDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).fileDao(); private final Gson gson = new Gson(); + public final OfflineOperationsRepositoryType offlineOperationsRepository; + private final static int DEFAULT_CURSOR_INT_VALUE = -1; public FileDataStorageManager(User user, ContentResolver contentResolver) { this.contentProviderClient = null; this.contentResolver = contentResolver; this.user = user; + offlineOperationsRepository = new OfflineOperationsRepository(this); } public FileDataStorageManager(User user, ContentProviderClient contentProviderClient) { this.contentProviderClient = contentProviderClient; this.contentResolver = null; this.user = user; + offlineOperationsRepository = new OfflineOperationsRepository(this); } /** @@ -124,6 +147,198 @@ public class FileDataStorageManager { return getFileByPath(ProviderTableMeta.FILE_PATH_DECRYPTED, path); } + public void addCreateFileOfflineOperation(String[] localPaths, String[] remotePaths) { + if (localPaths.length != remotePaths.length) { + Log_OC.d(TAG, "Local path and remote path size do not match"); + return; + } + + for (int i = 0; i < localPaths.length; i++) { + String localPath = localPaths[i]; + String remotePath = remotePaths[i]; + String mimeType = MimeTypeUtil.getMimeTypeFromPath(remotePath); + + OfflineOperationEntity entity = new OfflineOperationEntity(); + entity.setPath(remotePath); + entity.setType(new OfflineOperationType.CreateFile(OfflineOperationRawType.CreateFile.name(), localPath, remotePath, mimeType)); + + long createdAt = System.currentTimeMillis(); + long modificationTimestamp = System.currentTimeMillis(); + + entity.setCreatedAt(createdAt); + entity.setModifiedAt(modificationTimestamp / 1000); + entity.setFilename(new File(remotePath).getName()); + + String parentPath = new File(remotePath).getParent() + OCFile.PATH_SEPARATOR; + OCFile parentFile = getFileByDecryptedRemotePath(parentPath); + + if (parentFile != null) { + entity.setParentOCFileId(parentFile.getFileId()); + } + + offlineOperationDao.insert(entity); + createPendingFile(remotePath, mimeType, createdAt, modificationTimestamp); + } + } + + public OfflineOperationEntity getOfflineEntityFromOCFile(OCFile file) { + return offlineOperationDao.getByPath(file.getDecryptedRemotePath()); + } + + public OfflineOperationEntity addCreateFolderOfflineOperation(String path, String filename, Long parentOCFileId) { + OfflineOperationEntity entity = new OfflineOperationEntity(); + + entity.setFilename(filename); + entity.setParentOCFileId(parentOCFileId); + + OfflineOperationType.CreateFolder operationType = new OfflineOperationType.CreateFolder(OfflineOperationRawType.CreateFolder.name(), path); + entity.setType(operationType); + entity.setPath(path); + + long createdAt = System.currentTimeMillis(); + long modificationTimestamp = System.currentTimeMillis(); + + entity.setCreatedAt(createdAt); + entity.setModifiedAt(modificationTimestamp / 1000); + + offlineOperationDao.insert(entity); + createPendingDirectory(path, createdAt, modificationTimestamp); + + return entity; + } + + public void createPendingFile(String path, String mimeType, long createdAt, long modificationTimestamp) { + OCFile file = new OCFile(path); + file.setMimeType(mimeType); + file.setCreationTimestamp(createdAt); + file.setModificationTimestamp(modificationTimestamp); + saveFileWithParent(file, MainApp.getAppContext()); + } + + public void createPendingDirectory(String path, long createdAt, long modificationTimestamp) { + OCFile directory = new OCFile(path); + directory.setMimeType(MimeType.DIRECTORY); + directory.setCreationTimestamp(createdAt); + directory.setModificationTimestamp(modificationTimestamp); + saveFileWithParent(directory, MainApp.getAppContext()); + } + + public void deleteOfflineOperation(OCFile file) { + offlineOperationsRepository.deleteOperation(file); + } + + public void addRenameFileOfflineOperation(OCFile file, String newName) { + OfflineOperationEntity entity = new OfflineOperationEntity(); + + entity.setFilename(newName); + entity.setParentOCFileId(file.getParentId()); + + OfflineOperationType operationType = new OfflineOperationType.RenameFile(OfflineOperationRawType.RenameFile.name(), file.getFileId(), newName); + entity.setType(operationType); + entity.setPath(file.getDecryptedRemotePath()); + + long createdAt = System.currentTimeMillis(); + long modificationTimestamp = System.currentTimeMillis(); + + entity.setCreatedAt(createdAt); + entity.setModifiedAt(modificationTimestamp / 1000); + + offlineOperationDao.insert(entity); + } + + public String getFileNameBasedOnEncryptionStatus(OCFile file) { + FileEntity entity = fileDao.getFileById(file.getFileId()); + if (entity == null) { + return file.getFileName(); + } + + if (file.isEncrypted()) { + return entity.getEncryptedName(); + } else { + return entity.getName(); + } + } + + public String getFilenameConsideringOfflineOperation(OCFile file) { + String filename = file.getDecryptedFileName(); + OfflineOperationEntity renameEntity = offlineOperationDao.getByPath(file.getDecryptedRemotePath()); + if (renameEntity != null && renameEntity.getType() instanceof OfflineOperationType.RenameFile renameFile) { + filename = renameFile.getNewName(); + } + + return filename; + } + + public void addRemoveFileOfflineOperation(@NonNull OCFile file) { + OfflineOperationEntity entity = new OfflineOperationEntity(); + + String path = file.getDecryptedRemotePath(); + entity.setFilename(file.getFileName()); + entity.setParentOCFileId(file.getParentId()); + + OfflineOperationType.RemoveFile operationType = new OfflineOperationType.RemoveFile(OfflineOperationRawType.RemoveFile.name(), path); + entity.setType(operationType); + entity.setPath(path); + + long createdAt = System.currentTimeMillis(); + long modificationTimestamp = System.currentTimeMillis(); + + entity.setCreatedAt(createdAt); + entity.setModifiedAt(modificationTimestamp / 1000); + + offlineOperationDao.insert(entity); + } + + public void renameOfflineOperation(OCFile file, String newFolderName) { + var entity = offlineOperationDao.getByPath(file.getDecryptedRemotePath()); + if (entity == null) { + return; + } + + OCFile parentFolder = getFileById(file.getParentId()); + if (parentFolder == null) { + return; + } + + String newPath = parentFolder.getDecryptedRemotePath() + newFolderName + OCFile.PATH_SEPARATOR; + + if (entity.getType() instanceof OfflineOperationType.CreateFolder createFolderType) { + createFolderType.setPath(newPath); + } else if (entity.getType() instanceof OfflineOperationType.CreateFile createFileType) { + createFileType.setRemotePath(newPath); + createFileType.setMimeType(file.getMimeType()); + } + entity.setType(entity.getType()); + + entity.setPath(newPath); + entity.setFilename(newFolderName); + offlineOperationDao.update(entity); + + moveLocalFile(file, newPath, parentFolder.getDecryptedRemotePath()); + } + + @SuppressLint("SimpleDateFormat") + public void keepOfflineOperationAndServerFile(OfflineOperationEntity entity, OCFile file) { + if (file == null) return; + + String oldFileName = entity.getFilename(); + if (oldFileName == null) return; + + Long parentOCFileId = entity.getParentOCFileId(); + if (parentOCFileId == null) return; + + OCFile parentFolder = getFileById(parentOCFileId); + if (parentFolder == null) return; + + DateFormatPattern formatPattern = DateFormatPattern.FullDateWithHours; + String currentDateTime = DateExtensionsKt.currentDateRepresentation(new Date(), formatPattern); + + String newFolderName = oldFileName + " - " + currentDateTime; + String newPath = parentFolder.getDecryptedRemotePath() + newFolderName + OCFile.PATH_SEPARATOR; + moveLocalFile(file, newPath, parentFolder.getDecryptedRemotePath()); + offlineOperationsRepository.updateNextOperations(entity); + } + private @Nullable OCFile getFileByPath(String type, String path) { final boolean shouldUseEncryptedPath = ProviderTableMeta.FILE_PATH.equals(type); @@ -151,6 +366,15 @@ public class FileDataStorageManager { return null; } + public @Nullable + OCFile getFileByLocalId(long localId) { + FileEntity fileEntity = fileDao.getFileByLocalId(localId); + if (fileEntity != null) { + return createFileInstance(fileEntity); + } + return null; + } + public @Nullable OCFile getFileByLocalPath(String path) { FileEntity fileEntity = fileDao.getFileByLocalPath(path, user.getAccountName()); @@ -169,12 +393,19 @@ public class FileDataStorageManager { return null; } - public boolean fileExists(long id) { return fileDao.getFileById(id) != null; } + public boolean fileExists(long id) { + return fileDao.getFileById(id) != null; + } public boolean fileExists(String path) { return fileDao.getFileByEncryptedRemotePath(path, user.getAccountName()) != null; } + public OCFile getTopParent(OCFile file) { + long topParentId = getTopParentId(file); + return getFileById(topParentId); + } + public long getTopParentId(OCFile file) { if (file.getParentId() == 1) { return file.getFileId(); @@ -230,6 +461,23 @@ public class FileDataStorageManager { } } + public OCFile findDuplicatedFile(OCFile parentFolder, ServerFileInterface newFile) { + List folderContent = getFolderContent(parentFolder, false); + if (folderContent == null || folderContent.isEmpty()) { + return null; + } + + OCFile duplicatedFile = null; + for (OCFile file : folderContent) { + if (file.getFileName().equals(newFile.getFileName())) { + duplicatedFile = file; + break; + } + } + + return duplicatedFile; + } + public List getFolderImages(OCFile folder, boolean onlyOnDevice) { List imageList = new ArrayList<>(); @@ -325,7 +573,7 @@ public class FileDataStorageManager { } else { Exception exception = result.getException(); String message = "Error during saving file with parents: " + ocFile.getRemotePath() + " / " - + result.getLogMessage(); + + result.getLogMessage(context); if (exception != null) { throw new RemoteOperationFailedException(message, exception); @@ -345,19 +593,19 @@ public class FileDataStorageManager { } public static void clearTempEncryptedFolder(String accountName) { - File tempEncryptedFolder = new File(FileStorageUtils.getTemporalEncryptedFolderPath(accountName)); + File tempEncryptedFolder = new File(FileStorageUtils.getTemporalEncryptedFolderPath(accountName)); if (!tempEncryptedFolder.exists()) { - Log_OC.d(TAG,"tempEncryptedFolder does not exist"); + Log_OC.d(TAG, "tempEncryptedFolder does not exist"); return; } try { FileUtils.cleanDirectory(tempEncryptedFolder); - Log_OC.d(TAG,"tempEncryptedFolder cleared"); + Log_OC.d(TAG, "tempEncryptedFolder cleared"); } catch (IOException exception) { - Log_OC.d(TAG,"Error caught at clearTempEncryptedFolder: " + exception); + Log_OC.d(TAG, "Error caught at clearTempEncryptedFolder: " + exception); } } @@ -390,7 +638,7 @@ public class FileDataStorageManager { /** * Inserts or updates the list of files contained in a given folder. - * + *

* CALLER IS RESPONSIBLE FOR GRANTING RIGHT UPDATE OF INFORMATION, NOT THIS METHOD. HERE ONLY DATA CONSISTENCY * SHOULD BE GRANTED * @@ -438,7 +686,7 @@ public class FileDataStorageManager { whereArgs[1] = ocFile.getRemotePath(); if (ocFile.isFolder()) { operations.add(ContentProviderOperation.newDelete( - ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_DIR, ocFile.getFileId())) + ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_DIR, ocFile.getFileId())) .withSelection(where, whereArgs).build()); File localFolder = new File(FileStorageUtils.getDefaultSavePathFor(user.getAccountName(), ocFile)); @@ -447,7 +695,7 @@ public class FileDataStorageManager { } } else { operations.add(ContentProviderOperation.newDelete( - ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_FILE, ocFile.getFileId())) + ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_FILE, ocFile.getFileId())) .withSelection(where, whereArgs).build()); if (ocFile.isDown()) { @@ -507,14 +755,17 @@ public class FileDataStorageManager { /** * Returns a {@link ContentValues} filled with values that are common to both files and folders + * * @see #createContentValuesForFile(OCFile) * @see #createContentValuesForFolder(OCFile) */ + @SuppressFBWarnings("CE") private ContentValues createContentValuesBase(OCFile fileOrFolder) { final ContentValues cv = new ContentValues(); cv.put(ProviderTableMeta.FILE_MODIFIED, fileOrFolder.getModificationTimestamp()); cv.put(ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA, fileOrFolder.getModificationTimestampAtLastSyncForData()); cv.put(ProviderTableMeta.FILE_PARENT, fileOrFolder.getParentId()); + cv.put(ProviderTableMeta.FILE_UPLOADED, fileOrFolder.getUploadTimestamp()); cv.put(ProviderTableMeta.FILE_CREATION, fileOrFolder.getCreationTimestamp()); cv.put(ProviderTableMeta.FILE_CONTENT_TYPE, fileOrFolder.getMimeType()); cv.put(ProviderTableMeta.FILE_NAME, fileOrFolder.getFileName()); @@ -540,11 +791,14 @@ public class FileDataStorageManager { cv.put(ProviderTableMeta.FILE_SHAREES, gson.toJson(fileOrFolder.getSharees())); cv.put(ProviderTableMeta.FILE_TAGS, gson.toJson(fileOrFolder.getTags())); cv.put(ProviderTableMeta.FILE_RICH_WORKSPACE, fileOrFolder.getRichWorkspace()); + cv.put(ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP, fileOrFolder.getInternalFolderSyncTimestamp()); + cv.put(ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_RESULT, fileOrFolder.getInternalFolderSyncResult()); return cv; } /** * Returns a {@link ContentValues} filled with values for a folder + * * @see #createContentValuesForFile(OCFile) * @see #createContentValuesBase(OCFile) */ @@ -556,6 +810,7 @@ public class FileDataStorageManager { /** * Returns a {@link ContentValues} filled with values for a file + * * @see #createContentValuesForFolder(OCFile) * @see #createContentValuesBase(OCFile) */ @@ -584,6 +839,8 @@ public class FileDataStorageManager { cv.put(ProviderTableMeta.FILE_METADATA_GPS, gson.toJson(file.getGeoLocation())); cv.put(ProviderTableMeta.FILE_METADATA_LIVE_PHOTO, file.getLinkedFileIdForLivePhoto()); cv.put(ProviderTableMeta.FILE_E2E_COUNTER, file.getE2eCounter()); + cv.put(ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP, file.getInternalFolderSyncTimestamp()); + cv.put(ProviderTableMeta.FILE_INTERNAL_TWO_WAY_SYNC_RESULT, file.getInternalFolderSyncResult()); return cv; } @@ -725,7 +982,7 @@ public class FileDataStorageManager { /** * Updates database and file system for a file or folder that was moved to a different location. - * + *

* TODO explore better (faster) implementations TODO throw exceptions up ! */ public void moveLocalFile(OCFile ocFile, String targetPath, String targetParentPath) { @@ -750,7 +1007,7 @@ public class FileDataStorageManager { int lengthOfOldPath = oldPath.length(); int lengthOfOldStoragePath = defaultSavePath.length() + lengthOfOldPath; - for (FileEntity fileEntity: fileEntities) { + for (FileEntity fileEntity : fileEntities) { ContentValues contentValues = new ContentValues(); // keep construction in the loop OCFile childFile = createFileInstance(fileEntity); contentValues.put( @@ -853,8 +1110,8 @@ public class FileDataStorageManager { } /** - * This method does not require {@link FileDataStorageManager} being initialized - * with any specific user. Migration can be performed with {@link com.nextcloud.client.account.AnonymousUser}. + * This method does not require {@link FileDataStorageManager} being initialized with any specific user. Migration + * can be performed with {@link com.nextcloud.client.account.AnonymousUser}. */ public void migrateStoredFiles(String sourcePath, String destinationPath) throws RemoteException, OperationApplicationException { @@ -886,7 +1143,7 @@ public class FileDataStorageManager { ContentValues cv = new ContentValues(); fileId[0] = String.valueOf(cursor.getLong(cursor.getColumnIndexOrThrow(ProviderTableMeta._ID))); String oldFileStoragePath = - cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_STORAGE_PATH)); + cursor.getString(cursor.getColumnIndexOrThrow(ProviderTableMeta.FILE_STORAGE_PATH)); if (oldFileStoragePath.startsWith(sourcePath)) { @@ -917,7 +1174,7 @@ public class FileDataStorageManager { List folderContent = new ArrayList<>(); List files = fileDao.getFolderContent(parentId); - for (FileEntity fileEntity: files) { + for (FileEntity fileEntity : files) { OCFile child = createFileInstance(fileEntity); if (!onlyOnDevice || child.existsOnDevice()) { folderContent.add(child); @@ -928,7 +1185,6 @@ public class FileDataStorageManager { return folderContent; } - private OCFile createRootDir() { OCFile ocFile = new OCFile(OCFile.ROOT_PATH); ocFile.setMimeType(MimeType.DIRECTORY); @@ -953,7 +1209,11 @@ public class FileDataStorageManager { return (i == null) ? 0 : i; } - private OCFile createFileInstance(FileEntity fileEntity) { + private long nullToMinusOne(Long i) { + return (i == null) ? -1L : i; + } + + public OCFile createFileInstance(FileEntity fileEntity) { OCFile ocFile = new OCFile(fileEntity.getPath()); ocFile.setDecryptedRemotePath(fileEntity.getPathDecrypted()); ocFile.setFileId(nullToZero(fileEntity.getId())); @@ -974,6 +1234,7 @@ public class FileDataStorageManager { } } ocFile.setFileLength(nullToZero(fileEntity.getContentLength())); + ocFile.setUploadTimestamp(nullToZero(fileEntity.getUploaded())); ocFile.setCreationTimestamp(nullToZero(fileEntity.getCreation())); ocFile.setModificationTimestamp(nullToZero(fileEntity.getModified())); ocFile.setModificationTimestampAtLastSyncForData(nullToZero(fileEntity.getModifiedAtLastSyncForData())); @@ -1017,6 +1278,7 @@ public class FileDataStorageManager { ocFile.setLivePhoto(fileEntity.getMetadataLivePhoto()); ocFile.setHidden(nullToZero(fileEntity.getHidden()) == 1); ocFile.setE2eCounter(fileEntity.getE2eCounter()); + ocFile.setInternalFolderSyncTimestamp(nullToMinusOne(fileEntity.getInternalTwoWaySync())); String sharees = fileEntity.getSharees(); // Surprisingly JSON deserialization causes significant overhead. @@ -1040,7 +1302,7 @@ public class FileDataStorageManager { ocFile.setTags(new ArrayList<>()); } else { try { - String[] tagsArray = gson.fromJson(tags, String[].class); + Tag[] tagsArray = gson.fromJson(tags, Tag[].class); ocFile.setTags(new ArrayList<>(Arrays.asList(tagsArray))); } catch (JsonSyntaxException e) { // ignore saved value due to api change @@ -1179,7 +1441,7 @@ public class FileDataStorageManager { + ProviderTableMeta.OCSHARES_ACCOUNT_OWNER + "=?", new String[]{value, user.getAccountName()}, null - ); + ); } else { try { cursor = getContentProviderClient().query( @@ -1188,7 +1450,7 @@ public class FileDataStorageManager { key + AND + ProviderTableMeta.OCSHARES_ACCOUNT_OWNER + "=?", new String[]{value, user.getAccountName()}, null - ); + ); } catch (RemoteException e) { Log_OC.w(TAG, "Could not get details, assuming share does not exist: " + e.getMessage()); cursor = null; @@ -1303,6 +1565,11 @@ public class FileDataStorageManager { contentValues.put(ProviderTableMeta.OCSHARES_SHARE_LINK, share.getShareLink()); contentValues.put(ProviderTableMeta.OCSHARES_SHARE_LABEL, share.getLabel()); + FileDownloadLimit downloadLimit = share.getFileDownloadLimit(); + setDownloadLimitToContentValues(contentValues, downloadLimit); + + contentValues.put(ProviderTableMeta.OCSHARES_ATTRIBUTES, share.getAttributes()); + return contentValues; } @@ -1316,7 +1583,8 @@ public class FileDataStorageManager { share.setPermissions(getInt(cursor, ProviderTableMeta.OCSHARES_PERMISSIONS)); share.setSharedDate(getLong(cursor, ProviderTableMeta.OCSHARES_SHARED_DATE)); share.setExpirationDate(getLong(cursor, ProviderTableMeta.OCSHARES_EXPIRATION_DATE)); - share.setToken(getString(cursor, ProviderTableMeta.OCSHARES_TOKEN)); + String token = getString(cursor, ProviderTableMeta.OCSHARES_TOKEN); + share.setToken(token); share.setSharedWithDisplayName(getString(cursor, ProviderTableMeta.OCSHARES_SHARE_WITH_DISPLAY_NAME)); share.setFolder(getInt(cursor, ProviderTableMeta.OCSHARES_IS_DIRECTORY) == 1); share.setUserId(getString(cursor, ProviderTableMeta.OCSHARES_USER_ID)); @@ -1327,26 +1595,60 @@ public class FileDataStorageManager { share.setShareLink(getString(cursor, ProviderTableMeta.OCSHARES_SHARE_LINK)); share.setLabel(getString(cursor, ProviderTableMeta.OCSHARES_SHARE_LABEL)); + FileDownloadLimit fileDownloadLimit = getDownloadLimitFromCursor(cursor, token); + if (fileDownloadLimit != null) { + share.setFileDownloadLimit(fileDownloadLimit); + } + + share.setAttributes(getString(cursor, ProviderTableMeta.OCSHARES_ATTRIBUTES)); + return share; } - private void resetShareFlagsInAllFiles() { - ContentValues cv = new ContentValues(); - cv.put(ProviderTableMeta.FILE_SHARED_VIA_LINK, Boolean.FALSE); - cv.put(ProviderTableMeta.FILE_SHARED_WITH_SHAREE, Boolean.FALSE); - String where = ProviderTableMeta.FILE_ACCOUNT_OWNER + "=?"; - String[] whereArgs = new String[]{user.getAccountName()}; - - if (getContentResolver() != null) { - getContentResolver().update(ProviderTableMeta.CONTENT_URI, cv, where, whereArgs); - - } else { - try { - getContentProviderClient().update(ProviderTableMeta.CONTENT_URI, cv, where, whereArgs); - } catch (RemoteException e) { - Log_OC.e(TAG, "Exception in resetShareFlagsInAllFiles" + e.getMessage(), e); - } + private void setDownloadLimitToContentValues(ContentValues contentValues, FileDownloadLimit downloadLimit) { + if (downloadLimit != null) { + contentValues.put(ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_LIMIT, downloadLimit.getLimit()); + contentValues.put(ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_COUNT, downloadLimit.getCount()); + return; } + + contentValues.putNull(ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_LIMIT); + contentValues.putNull(ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_COUNT); + } + + @Nullable + private FileDownloadLimit getDownloadLimitFromCursor(Cursor cursor, String token) { + if (token == null || cursor == null) { + return null; + } + + int limit = getIntOrDefault(cursor, ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_LIMIT); + int count = getIntOrDefault(cursor, ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_COUNT); + if (limit != DEFAULT_CURSOR_INT_VALUE && count != DEFAULT_CURSOR_INT_VALUE) { + return new FileDownloadLimit(token, limit, count); + } + + return null; + } + + /** + * Retrieves an integer value from the specified column in the cursor. + *

+ * If the column does not exist (i.e., {@code cursor.getColumnIndex(columnName)} returns -1), + * this method returns {@code -1} as a default value. + *

+ * + * @param cursor The Cursor from which to retrieve the value. + * @param columnName The name of the column to retrieve the integer from. + * @return The integer value from the column, or {@code -1} if the column is not found. + */ + private int getIntOrDefault(Cursor cursor, String columnName) { + int index = cursor.getColumnIndex(columnName); + if (index == DEFAULT_CURSOR_INT_VALUE) { + return DEFAULT_CURSOR_INT_VALUE; + } + + return cursor.getInt(index); } private void resetShareFlagsInFolder(OCFile folder) { @@ -1427,21 +1729,19 @@ public class FileDataStorageManager { ContentProviderOperation.newInsert(ProviderTableMeta.CONTENT_URI_SHARE) .withValues(contentValues) .build() - ); + ); } } // apply operations in batch if (operations.size() > 0) { - @SuppressWarnings("unused") - ContentProviderResult[] results = null; Log_OC.d(TAG, String.format(Locale.ENGLISH, SENDING_TO_FILECONTENTPROVIDER_MSG, operations.size())); try { if (getContentResolver() != null) { - results = getContentResolver().applyBatch(MainApp.getAuthority(), + getContentResolver().applyBatch(MainApp.getAuthority(), operations); } else { - results = getContentProviderClient().applyBatch(operations); + getContentProviderClient().applyBatch(operations); } } catch (OperationApplicationException | RemoteException e) { @@ -1467,6 +1767,67 @@ public class FileDataStorageManager { } } + public void saveSharesFromRemoteFile(List shares) { + if (shares == null || shares.isEmpty()) { + return; + } + + // Prepare reset operations + Set uniquePaths = new HashSet<>(); + for (RemoteFile share : shares) { + uniquePaths.add(share.getRemotePath()); + } + + ArrayList resetOperations = new ArrayList<>(); + for (String path : uniquePaths) { + resetShareFlagInAFile(path); + var removeOps = prepareRemoveSharesInFile(path, new ArrayList<>()); + if (!removeOps.isEmpty()) { + resetOperations.addAll(removeOps); + } + } + if (!resetOperations.isEmpty()) { + applyBatch(resetOperations); + } + + // Prepare insert operations + ArrayList insertOperations = prepareInsertSharesFromRemoteFile(shares); + if (!insertOperations.isEmpty()) { + applyBatch(insertOperations); + } + } + + /** + * Prepares a list of ContentProviderOperation insert operations based on share information + * found in the given iterable of RemoteFile objects. + *

+ * Each RemoteFile may have multiple share entries (sharees), and for each one, + * a corresponding ContentProviderOperation is created for insertion into the shares table. + * + * @param remoteFiles An iterable list of RemoteFile objects containing sharee data. + * @return A list of ContentProviderOperation objects for batch insertion into the content provider. + */ + private ArrayList prepareInsertSharesFromRemoteFile(Iterable remoteFiles) { + final ArrayList contentValueList = new ArrayList<>(); + for (RemoteFile remoteFile : remoteFiles) { + final var contentValues = ShareeEntry.Companion.getContentValues(remoteFile, user.getAccountName()); + if (contentValues == null) { + continue; + } + contentValueList.addAll(contentValues); + } + + ArrayList operations = new ArrayList<>(); + for (ContentValues contentValues : contentValueList) { + operations.add(ContentProviderOperation + .newInsert(ProviderTableMeta.CONTENT_URI_SHARE) + .withValues(contentValues) + .build()); + } + + return operations; + } + public void saveSharesDB(List shares) { ArrayList operations = new ArrayList<>(); @@ -1483,20 +1844,26 @@ public class FileDataStorageManager { // Add operations to insert shares operations = prepareInsertShares(shares, operations); + if (operations.isEmpty()) { + return; + } + // apply operations in batch - if (operations.size() > 0) { - Log_OC.d(TAG, String.format(Locale.ENGLISH, SENDING_TO_FILECONTENTPROVIDER_MSG, operations.size())); - try { - if (getContentResolver() != null) { - getContentResolver().applyBatch(MainApp.getAuthority(), operations); + Log_OC.d(TAG, String.format(Locale.ENGLISH, SENDING_TO_FILECONTENTPROVIDER_MSG, operations.size())); + applyBatch(operations); + } - } else { - getContentProviderClient().applyBatch(operations); - } + private void applyBatch(ArrayList operations) { + try { + if (getContentResolver() != null) { + getContentResolver().applyBatch(MainApp.getAuthority(), operations); - } catch (OperationApplicationException | RemoteException e) { - Log_OC.e(TAG, EXCEPTION_MSG + e.getMessage(), e); + } else { + getContentProviderClient().applyBatch(operations); } + + } catch (OperationApplicationException | RemoteException e) { + Log_OC.e(TAG, EXCEPTION_MSG + e.getMessage(), e); } } @@ -1554,8 +1921,7 @@ public class FileDataStorageManager { * @param operations List of operations * @return */ - private ArrayList prepareInsertShares( - Iterable shares, ArrayList operations) { + private ArrayList prepareInsertShares(Iterable shares, ArrayList operations) { ContentValues contentValues; // prepare operations to insert or update files to save in the given folder @@ -1587,7 +1953,7 @@ public class FileDataStorageManager { ContentProviderOperation.newDelete(ProviderTableMeta.CONTENT_URI_SHARE). withSelection(where, whereArgs). build() - ); + ); } } return preparedOperations; @@ -1605,7 +1971,7 @@ public class FileDataStorageManager { .newDelete(ProviderTableMeta.CONTENT_URI_SHARE) .withSelection(where, whereArgs) .build() - ); + ); return preparedOperations; @@ -1619,17 +1985,19 @@ public class FileDataStorageManager { + ProviderTableMeta.OCSHARES_SHARE_TYPE + " = ? OR " + ProviderTableMeta.OCSHARES_SHARE_TYPE + " = ? OR " + ProviderTableMeta.OCSHARES_SHARE_TYPE + " = ? OR " + + ProviderTableMeta.OCSHARES_SHARE_TYPE + " = ? OR " + ProviderTableMeta.OCSHARES_SHARE_TYPE + " = ? ) "; String[] selectionArgs = new String[]{filePath, accountName, Integer.toString(ShareType.USER.getValue()), Integer.toString(ShareType.GROUP.getValue()), Integer.toString(ShareType.EMAIL.getValue()), Integer.toString(ShareType.FEDERATED.getValue()), + Integer.toString(ShareType.FEDERATED_GROUP.getValue()), Integer.toString(ShareType.ROOM.getValue()), Integer.toString(ShareType.CIRCLE.getValue()) }; - Cursor cursor = null; + Cursor cursor; if (getContentResolver() != null) { cursor = getContentResolver().query(ProviderTableMeta.CONTENT_URI_SHARE, null, @@ -1756,7 +2124,7 @@ public class FileDataStorageManager { cv, ProviderTableMeta._ID + "=?", new String[]{String.valueOf(ocFile.getFileId())} - ); + ); } else { try { updated = getContentProviderClient().update( @@ -1764,7 +2132,7 @@ public class FileDataStorageManager { cv, ProviderTableMeta._ID + "=?", new String[]{String.valueOf(ocFile.getFileId())} - ); + ); } catch (RemoteException e) { Log_OC.e(TAG, "Failed saving conflict in database " + e.getMessage(), e); } @@ -1793,20 +2161,20 @@ public class FileDataStorageManager { stringBuilder.append("?)"); if (getContentResolver() != null) { - updated = getContentResolver().update( + getContentResolver().update( ProviderTableMeta.CONTENT_URI_FILE, cv, stringBuilder.toString(), ancestorIds.toArray(new String[]{}) - ); + ); } else { try { - updated = getContentProviderClient().update( + getContentProviderClient().update( ProviderTableMeta.CONTENT_URI_FILE, cv, stringBuilder.toString(), ancestorIds.toArray(new String[]{}) - ); + ); } catch (RemoteException e) { Log_OC.e(TAG, "Failed saving conflict in database " + e.getMessage(), e); } @@ -1838,7 +2206,7 @@ public class FileDataStorageManager { whereForDescencentsInConflict, new String[]{user.getAccountName(), parentPath + '%'}, null - ); + ); } else { try { descendentsInConflict = getContentProviderClient().query( @@ -1847,7 +2215,7 @@ public class FileDataStorageManager { whereForDescencentsInConflict, new String[]{user.getAccountName(), parentPath + "%"}, null - ); + ); } catch (RemoteException e) { Log_OC.e(TAG, "Failed querying for descendents in conflict " + e.getMessage(), e); } @@ -1856,22 +2224,22 @@ public class FileDataStorageManager { if (descendentsInConflict == null || descendentsInConflict.getCount() == 0) { Log_OC.d(TAG, "NO MORE conflicts in " + parentPath); if (getContentResolver() != null) { - updated = getContentResolver().update( + getContentResolver().update( ProviderTableMeta.CONTENT_URI_FILE, cv, ProviderTableMeta.FILE_ACCOUNT_OWNER + AND + ProviderTableMeta.FILE_PATH + "=?", new String[]{user.getAccountName(), parentPath} - ); + ); } else { try { - updated = getContentProviderClient().update( + getContentProviderClient().update( ProviderTableMeta.CONTENT_URI_FILE, cv, ProviderTableMeta.FILE_ACCOUNT_OWNER + AND + ProviderTableMeta.FILE_PATH + "=?" , new String[]{user.getAccountName(), parentPath} - ); + ); } catch (RemoteException e) { Log_OC.e(TAG, "Failed saving conflict in database " + e.getMessage(), e); } @@ -2030,6 +2398,8 @@ public class FileDataStorageManager { contentValues.put(ProviderTableMeta.CAPABILITIES_USER_STATUS, capability.getUserStatus().getValue()); contentValues.put(ProviderTableMeta.CAPABILITIES_USER_STATUS_SUPPORTS_EMOJI, capability.getUserStatusSupportsEmoji().getValue()); + contentValues.put(ProviderTableMeta.CAPABILITIES_USER_STATUS_SUPPORTS_BUSY, + capability.getUserStatusSupportsBusy().getValue()); contentValues.put(ProviderTableMeta.CAPABILITIES_FILES_LOCKING_VERSION, capability.getFilesLockingVersion()); contentValues.put(ProviderTableMeta.CAPABILITIES_ASSISTANT, capability.getAssistant().getValue()); @@ -2037,6 +2407,22 @@ public class FileDataStorageManager { contentValues.put(ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT, capability.getDropAccount().getValue()); contentValues.put(ProviderTableMeta.CAPABILITIES_SECURITY_GUARD, capability.getSecurityGuard().getValue()); + contentValues.put(ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAME_CHARACTERS, capability.getForbiddenFilenameCharactersJson()); + contentValues.put(ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAMES, capability.getForbiddenFilenamesJson()); + contentValues.put(ProviderTableMeta.CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_EXTENSIONS, capability.getForbiddenFilenameExtensionJson()); + contentValues.put(ProviderTableMeta.CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_BASE_NAMES, capability.getForbiddenFilenameBaseNamesJson()); + contentValues.put(ProviderTableMeta.CAPABILITIES_WINDOWS_COMPATIBLE_FILENAMES, capability.isWCFEnabled().getValue()); + contentValues.put(ProviderTableMeta.CAPABILITIES_FILES_DOWNLOAD_LIMIT, capability.getFilesDownloadLimit().getValue()); + contentValues.put(ProviderTableMeta.CAPABILITIES_FILES_DOWNLOAD_LIMIT_DEFAULT, capability.getFilesDownloadLimitDefault()); + + contentValues.put(ProviderTableMeta.CAPABILITIES_RECOMMENDATION, capability.getRecommendations().getValue()); + + contentValues.put(ProviderTableMeta.CAPABILITIES_NOTES_FOLDER_PATH, capability.getNotesFolderPath()); + + contentValues.put(ProviderTableMeta.CAPABILITIES_DEFAULT_PERMISSIONS, capability.getDefaultPermissions()); + + contentValues.put(ProviderTableMeta.CAPABILITIES_HAS_VALID_SUBSCRIPTION, capability.getHasValidSubscription().getValue()); + return contentValues; } @@ -2199,13 +2585,31 @@ public class FileDataStorageManager { capability.setUserStatus(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_USER_STATUS)); capability.setUserStatusSupportsEmoji( getBoolean(cursor, ProviderTableMeta.CAPABILITIES_USER_STATUS_SUPPORTS_EMOJI)); + capability.setUserStatusSupportsBusy( + getBoolean(cursor, ProviderTableMeta.CAPABILITIES_USER_STATUS_SUPPORTS_BUSY)); capability.setFilesLockingVersion( getString(cursor, ProviderTableMeta.CAPABILITIES_FILES_LOCKING_VERSION)); capability.setAssistant(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_ASSISTANT)); capability.setGroupfolders(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_GROUPFOLDERS)); capability.setDropAccount(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_DROP_ACCOUNT)); capability.setSecurityGuard(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_SECURITY_GUARD)); + + capability.setForbiddenFilenameCharactersJson(getString(cursor, ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAME_CHARACTERS)); + capability.setForbiddenFilenamesJson(getString(cursor, ProviderTableMeta.CAPABILITIES_FORBIDDEN_FILENAMES)); + capability.setForbiddenFilenameExtensionJson(getString(cursor, ProviderTableMeta.CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_EXTENSIONS)); + capability.setForbiddenFilenameBaseNamesJson(getString(cursor, ProviderTableMeta.CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_BASE_NAMES)); + capability.setWCFEnabled(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_WINDOWS_COMPATIBLE_FILENAMES)); + capability.setFilesDownloadLimit(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_FILES_DOWNLOAD_LIMIT)); + capability.setFilesDownloadLimitDefault(getInt(cursor, ProviderTableMeta.CAPABILITIES_FILES_DOWNLOAD_LIMIT_DEFAULT)); + + capability.setRecommendations(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_RECOMMENDATION)); + + capability.setNotesFolderPath(getString(cursor, ProviderTableMeta.CAPABILITIES_NOTES_FOLDER_PATH)); + + capability.setDefaultPermissions(getInt(cursor, ProviderTableMeta.CAPABILITIES_DEFAULT_PERMISSIONS)); + capability.setHasValidSubscription(getBoolean(cursor, ProviderTableMeta.CAPABILITIES_HAS_VALID_SUBSCRIPTION)); } + return capability; } @@ -2250,7 +2654,7 @@ public class FileDataStorageManager { Log_OC.d(TAG, "getGalleryItems - query complete, list size: " + fileEntities.size()); List files = new ArrayList<>(fileEntities.size()); - for (FileEntity fileEntity: fileEntities) { + for (FileEntity fileEntity : fileEntities) { files.add(createFileInstance(fileEntity)); } @@ -2271,7 +2675,7 @@ public class FileDataStorageManager { ProviderTableMeta.VIRTUAL_TYPE + "=?", new String[]{String.valueOf(type)}, null - ); + ); } catch (RemoteException e) { Log_OC.e(TAG, e.getMessage(), e); return ocFiles; @@ -2283,7 +2687,7 @@ public class FileDataStorageManager { ProviderTableMeta.VIRTUAL_TYPE + "=?", new String[]{String.valueOf(type)}, null - ); + ); } if (c != null) { @@ -2362,7 +2766,7 @@ public class FileDataStorageManager { List files = getAllFilesRecursivelyInsideFolder(folder); List> decryptedFileNamesAndEncryptedRemotePaths = getDecryptedFileNamesAndEncryptedRemotePaths(files); - String decryptedFileName = decryptedRemotePath.substring( decryptedRemotePath.lastIndexOf('/') + 1); + String decryptedFileName = decryptedRemotePath.substring(decryptedRemotePath.lastIndexOf('/') + 1); for (Pair item : decryptedFileNamesAndEncryptedRemotePaths) { if (item.getFirst().equals(decryptedFileName)) { @@ -2399,7 +2803,7 @@ public class FileDataStorageManager { List fileEntities = fileDao.getAllFiles(user.getAccountName()); List folderContent = new ArrayList<>(fileEntities.size()); - for (FileEntity fileEntity: fileEntities) { + for (FileEntity fileEntity : fileEntities) { folderContent.add(createFileInstance(fileEntity)); } @@ -2448,4 +2852,68 @@ public class FileDataStorageManager { return files; } + + public List getInternalTwoWaySyncFolders(User user) { + List fileEntities = fileDao.getInternalTwoWaySyncFolders(user.getAccountName()); + List files = new ArrayList<>(fileEntities.size()); + + for (FileEntity fileEntity : fileEntities) { + OCFile file = createFileInstance(fileEntity); + if (file.isFolder() && !file.isRootDirectory()) { + files.add(file); + } + } + + return files; + } + + public boolean isPartOfInternalTwoWaySync(OCFile file) { + if (file.isInternalFolderSync()) { + return true; + } + + while (file != null && !OCFile.ROOT_PATH.equals(file.getDecryptedRemotePath())) { + if (file.isInternalFolderSync()) { + return true; + } + file = getFileById(file.getParentId()); + } + return false; + } + + public List filter(OCFile file, OCFileFilterType filterType) { + if (!file.isRootDirectory()) { + return getFolderContent(file,false); + } + + final List result = new ArrayList<>(); + final List allFiles = getAllFiles(); + for (OCFile ocFile: allFiles) { + boolean condition = false; + if (filterType == OCFileFilterType.Shared) { + condition = ocFile.isShared(); + } else if (filterType == OCFileFilterType.Favorite) { + condition = ocFile.isFavorite(); + } + + if (condition) { + result.add(ocFile); + } + } + + return result; + } + + @Nullable + public FileEntity getFileEntity(OCFile file) { + if (file == null) { + return null; + } + + return fileDao.getFileById(file.getFileId()); + } + + public void updateFileEntity(@NonNull FileEntity entity) { + fileDao.update(entity); + } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileSystemDataSet.java b/app/src/main/java/com/owncloud/android/datamodel/FileSystemDataSet.java index 86b05fb..44f768c 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileSystemDataSet.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileSystemDataSet.java @@ -7,7 +7,7 @@ * Copyright (C) 2017 Nextcloud * Copyright (C) 2018 Andy Scherzinger * - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel; diff --git a/app/src/main/java/com/owncloud/android/datamodel/FilesystemDataProvider.java b/app/src/main/java/com/owncloud/android/datamodel/FilesystemDataProvider.java index aec6fda..e1a2016 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FilesystemDataProvider.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FilesystemDataProvider.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2017 Mario Danic * SPDX-FileCopyrightText: 2017 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel; @@ -14,15 +14,11 @@ import android.net.Uri; import com.owncloud.android.db.ProviderMeta; import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.utils.SyncedFolderUtils; import java.io.BufferedInputStream; -import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; -import java.util.HashSet; -import java.util.Set; import java.util.zip.CRC32; /** @@ -32,80 +28,27 @@ public class FilesystemDataProvider { static private final String TAG = FilesystemDataProvider.class.getSimpleName(); - private ContentResolver contentResolver; + private final ContentResolver contentResolver; public FilesystemDataProvider(ContentResolver contentResolver) { if (contentResolver == null) { + Log_OC.e(TAG, "couldn't be able constructed, contentResolver is null"); throw new IllegalArgumentException("Cannot create an instance with a NULL contentResolver"); } this.contentResolver = contentResolver; } public int deleteAllEntriesForSyncedFolder(String syncedFolderId) { + Log_OC.d(TAG, "deleteAllEntriesForSyncedFolder called, ID: " + syncedFolderId); + return contentResolver.delete( ProviderMeta.ProviderTableMeta.CONTENT_URI_FILESYSTEM, ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID + " = ?", - new String[]{syncedFolderId} - ); - } - - public void updateFilesystemFileAsSentForUpload(String path, String syncedFolderId) { - ContentValues cv = new ContentValues(); - cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD, 1); - - contentResolver.update( - ProviderMeta.ProviderTableMeta.CONTENT_URI_FILESYSTEM, - cv, - ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH + " = ? and " + - ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID + " = ?", - new String[]{path, syncedFolderId} - ); - } - - public Set getFilesForUpload(String localPath, String syncedFolderId) { - Set localPathsToUpload = new HashSet<>(); - - String likeParam = localPath + "%"; - - Cursor cursor = contentResolver.query( - ProviderMeta.ProviderTableMeta.CONTENT_URI_FILESYSTEM, - null, - ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH + " LIKE ? and " + - ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID + " = ? and " + - ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD + " = ? and " + - ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER + " = ?", - new String[]{likeParam, syncedFolderId, "0", "0"}, - null); - - if (cursor != null) { - if (cursor.moveToFirst()) { - do { - String value = cursor.getString(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH)); - if (value == null) { - Log_OC.e(TAG, "Cannot get local path"); - } else { - File file = new File(value); - if (!file.exists()) { - Log_OC.d(TAG, "Ignoring file for upload (doesn't exist): " + value); - } else if (!SyncedFolderUtils.isQualifiedFolder(file.getParent())) { - Log_OC.d(TAG, "Ignoring file for upload (unqualified folder): " + value); - } else if (!SyncedFolderUtils.isFileNameQualifiedForAutoUpload(file.getName())) { - Log_OC.d(TAG, "Ignoring file for upload (unqualified file): " + value); - } else { - localPathsToUpload.add(value); - } - } - } while (cursor.moveToNext()); - } - - cursor.close(); - } - - return localPathsToUpload; + new String[]{syncedFolderId}); } public void storeOrUpdateFileValue(String localPath, long modifiedAt, boolean isFolder, SyncedFolder syncedFolder) { + Log_OC.d(TAG, "storeOrUpdateFileValue called, localPath: " + localPath + " ID: " + syncedFolder.getId()); // takes multiple milliseconds to query data from database (around 75% of execution time) (6ms) FileSystemDataSet data = getFilesystemDataSet(localPath, syncedFolder); @@ -120,6 +63,7 @@ public class FilesystemDataProvider { cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_MODIFIED, modifiedAt); if (data == null) { + Log_OC.d(TAG, "storeOrUpdateFileValue data is null"); cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH, localPath); cv.put(ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER, isFolderValue); @@ -134,9 +78,10 @@ public class FilesystemDataProvider { Uri result = contentResolver.insert(ProviderMeta.ProviderTableMeta.CONTENT_URI_FILESYSTEM, cv); if (result == null) { - Log_OC.v(TAG, "Failed to insert filesystem data with local path: " + localPath); + Log_OC.e(TAG, "Failed to insert filesystem data with local path: " + localPath); } } else { + Log_OC.d(TAG, "storeOrUpdateFileValue data is not null"); if (data.getModifiedAt() != modifiedAt) { long newCrc32 = getFileChecksum(localPath); @@ -155,64 +100,69 @@ public class FilesystemDataProvider { ); if (result == 0) { - Log_OC.v(TAG, "Failed to update filesystem data with local path: " + localPath); + Log_OC.e(TAG, "Failed to update filesystem data with local path: " + localPath); } } } private FileSystemDataSet getFilesystemDataSet(String localPathParam, SyncedFolder syncedFolder) { + Log_OC.d(TAG, "getFilesForUpload called, localPath: " + localPathParam + " ID: " + syncedFolder.getId()); - Cursor cursor = contentResolver.query( + String[] projection = { + ProviderMeta.ProviderTableMeta._ID, + ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH, + ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_MODIFIED, + ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER, + ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_FOUND_RECENTLY, + ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD, + ProviderMeta.ProviderTableMeta.FILESYSTEM_CRC32 + }; + + String selection = ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH + " = ? AND " + + ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID + " = ?"; + String[] selectionArgs = { localPathParam, String.valueOf(syncedFolder.getId()) }; + + try (Cursor cursor = contentResolver.query( ProviderMeta.ProviderTableMeta.CONTENT_URI_FILESYSTEM, - null, - ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH + " = ? and " + - ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID + " = ?", - new String[]{localPathParam, Long.toString(syncedFolder.getId())}, + projection, + selection, + selectionArgs, null - ); - - FileSystemDataSet dataSet = null; - if (cursor != null) { - if (cursor.moveToFirst()) { + )) { + if (cursor != null && cursor.moveToFirst()) { int id = cursor.getInt(cursor.getColumnIndexOrThrow(ProviderMeta.ProviderTableMeta._ID)); + if (id == -1) { + Log_OC.e(TAG, "Arbitrary value could not be created from cursor"); + return null; + } + String localPath = cursor.getString(cursor.getColumnIndexOrThrow( ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH)); long modifiedAt = cursor.getLong(cursor.getColumnIndexOrThrow( ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_MODIFIED)); - boolean isFolder = false; - if (cursor.getInt(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER)) != 0) { - isFolder = true; - } - long foundAt = cursor.getLong(cursor.getColumnIndexOrThrow(ProviderMeta. - ProviderTableMeta.FILESYSTEM_FILE_FOUND_RECENTLY)); + boolean isFolder = cursor.getInt(cursor.getColumnIndexOrThrow( + ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER)) != 0; + long foundAt = cursor.getLong(cursor.getColumnIndexOrThrow( + ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_FOUND_RECENTLY)); + boolean isSentForUpload = cursor.getInt(cursor.getColumnIndexOrThrow( + ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD)) != 0; + String crc32 = cursor.getString(cursor.getColumnIndexOrThrow( + ProviderMeta.ProviderTableMeta.FILESYSTEM_CRC32)); - boolean isSentForUpload = false; - if (cursor.getInt(cursor.getColumnIndexOrThrow( - ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD)) != 0) { - isSentForUpload = true; - } - - String crc32 = cursor.getString(cursor.getColumnIndexOrThrow(ProviderMeta.ProviderTableMeta.FILESYSTEM_CRC32)); - - if (id == -1) { - Log_OC.e(TAG, "Arbitrary value could not be created from cursor"); - } else { - dataSet = new FileSystemDataSet(id, localPath, modifiedAt, isFolder, isSentForUpload, foundAt, - syncedFolder.getId(), crc32); - } + return new FileSystemDataSet(id, localPath, modifiedAt, isFolder, isSentForUpload, foundAt, + syncedFolder.getId(), crc32); } - cursor.close(); - } else { - Log_OC.e(TAG, "DB error restoring arbitrary values."); + } catch (Exception e) { + Log_OC.e(TAG, "DB error restoring arbitrary values.", e); } - return dataSet; + return null; } private long getFileChecksum(String filepath) { - try (InputStream inputStream = new BufferedInputStream(new FileInputStream(filepath))) { + try (FileInputStream fileInputStream = new FileInputStream(filepath); + InputStream inputStream = new BufferedInputStream(fileInputStream)) { CRC32 crc = new CRC32(); byte[] buf = new byte[1024 * 64]; int size; diff --git a/app/src/main/java/com/owncloud/android/datamodel/ForegroundServiceType.kt b/app/src/main/java/com/owncloud/android/datamodel/ForegroundServiceType.kt index 9e65d6f..46bb5ee 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/ForegroundServiceType.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/ForegroundServiceType.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel @@ -18,14 +18,13 @@ import androidx.annotation.RequiresApi * This wrapper is designed for compatibility on those versions. */ enum class ForegroundServiceType { - DataSync, MediaPlayback; + DataSync, + MediaPlayback; @RequiresApi(Build.VERSION_CODES.Q) - fun getId(): Int { - return if (this == DataSync) { - ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC - } else { - ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK - } + fun getId(): Int = if (this == DataSync) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + } else { + ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/GalleryItems.kt b/app/src/main/java/com/owncloud/android/datamodel/GalleryItems.kt index 363d6a4..44cb317 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/GalleryItems.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/GalleryItems.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel diff --git a/app/src/main/java/com/owncloud/android/datamodel/GalleryRow.kt b/app/src/main/java/com/owncloud/android/datamodel/GalleryRow.kt index 4e7a7aa..febd3aa 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/GalleryRow.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/GalleryRow.kt @@ -1,14 +1,18 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel +import com.nextcloud.utils.OCFileUtils + data class GalleryRow(val files: List, val defaultHeight: Int, val defaultWidth: Int) { - fun getMaxHeight(): Float { - return files.map { it.imageDimension?.height ?: defaultHeight.toFloat() }.maxOrNull() ?: 0f - } + fun getMaxHeight(): Float = files.maxOfOrNull { + OCFileUtils.getImageSize(it, defaultHeight.toFloat()).second.toFloat() + } ?: 0f + fun calculateHashCode(): Long = files.sumOf { it.hashCode() }.toLong() } diff --git a/app/src/main/java/com/owncloud/android/datamodel/MediaFolder.kt b/app/src/main/java/com/owncloud/android/datamodel/MediaFolder.kt index 028d28f..0ebb02d 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/MediaFolder.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/MediaFolder.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2016 Andy Scherzinger * SPDX-FileCopyrightText: 2016 Nextcloud - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel diff --git a/app/src/main/java/com/owncloud/android/datamodel/MediaFolderType.kt b/app/src/main/java/com/owncloud/android/datamodel/MediaFolderType.kt index b5d2dcc..8b98a18 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/MediaFolderType.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/MediaFolderType.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2017 Andy Scherzinger - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel @@ -12,7 +12,9 @@ import android.util.SparseArray * Types of media folder. */ enum class MediaFolderType(@JvmField val id: Int) { - CUSTOM(0), IMAGE(1), VIDEO(2); + CUSTOM(0), + IMAGE(1), + VIDEO(2); companion object { private val reverseMap = SparseArray(3) @@ -24,8 +26,6 @@ enum class MediaFolderType(@JvmField val id: Int) { } @JvmStatic - fun getById(id: Int?): MediaFolderType { - return reverseMap[id!!] - } + fun getById(id: Int?): MediaFolderType = reverseMap[id!!] } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/MediaFoldersModel.kt b/app/src/main/java/com/owncloud/android/datamodel/MediaFoldersModel.kt index b4d95f1..dc08c09 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/MediaFoldersModel.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/MediaFoldersModel.kt @@ -6,7 +6,7 @@ * Copyright (C) 2018 Mario Danic * Copyright (C) 2018 Andy Scherzinger * - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel diff --git a/app/src/main/java/com/owncloud/android/datamodel/MediaProvider.java b/app/src/main/java/com/owncloud/android/datamodel/MediaProvider.java index 074d6c4..c3dd31c 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/MediaProvider.java +++ b/app/src/main/java/com/owncloud/android/datamodel/MediaProvider.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2016 Andy Scherzinger * SPDX-FileCopyrightText: 2016 Nextcloud - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel; diff --git a/app/src/main/java/com/owncloud/android/datamodel/OCFile.java b/app/src/main/java/com/owncloud/android/datamodel/OCFile.java index c6bba5a..11455a2 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/OCFile.java +++ b/app/src/main/java/com/owncloud/android/datamodel/OCFile.java @@ -1,7 +1,7 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2022 Álvaro Brey Vilas * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2018 Andy Scherzinger @@ -9,7 +9,7 @@ * SPDX-FileCopyrightText: 2016 ownCloud Inc. * SPDX-FileCopyrightText: 2012-2016 David A. Velasco * SPDX-FileCopyrightText: 2012 Bartosz Przybylski - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.datamodel; @@ -20,36 +20,45 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; -import com.owncloud.android.BuildConfig; +import com.nextcloud.utils.BuildHelper; +import com.nextcloud.utils.extensions.StringExtensionsKt; import com.owncloud.android.R; import com.owncloud.android.lib.common.network.WebdavEntry; -import com.owncloud.android.lib.common.network.WebdavUtils; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.files.model.FileLockType; import com.owncloud.android.lib.resources.files.model.GeoLocation; import com.owncloud.android.lib.resources.files.model.ImageDimension; import com.owncloud.android.lib.resources.files.model.ServerFileInterface; import com.owncloud.android.lib.resources.shares.ShareeUser; +import com.owncloud.android.lib.resources.tags.Tag; import com.owncloud.android.utils.MimeType; import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import androidx.core.content.FileProvider; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import third_parties.daveKoeller.AlphanumComparator; public class OCFile implements Parcelable, Comparable, ServerFileInterface { - private final static String PERMISSION_SHARED_WITH_ME = "S"; - @VisibleForTesting public final static String PERMISSION_CAN_RESHARE = "R"; - private final static String PERMISSION_CAN_WRITE = "CK"; - private final static String PERMISSION_GROUPFOLDER = "M"; + private final static String PERMISSION_SHARED = "S"; + private final static String PERMISSION_MOUNTED = "M"; + private final static String PERMISSION_CAN_CREATE_FILE_INSIDE_FOLDER = "C"; + private final static String PERMISSION_CAN_CREATE_FOLDER_INSIDE_FOLDER = "K"; + private final static String PERMISSION_CAN_READ = "G"; + private final static String PERMISSION_CAN_WRITE = "W"; + private final static String PERMISSION_CAN_DELETE_OR_LEAVE_SHARE = "D"; + private final static String PERMISSION_CAN_RENAME = "N"; + private final static String PERMISSION_CAN_MOVE = "V"; + private final static String PERMISSION_CAN_CREATE_FILE_AND_FOLDER = PERMISSION_CAN_CREATE_FILE_INSIDE_FOLDER + PERMISSION_CAN_CREATE_FOLDER_INSIDE_FOLDER; + + private final static int MAX_FILE_SIZE_FOR_IMMEDIATE_PREVIEW_BYTES = 1024000; public static final String PATH_SEPARATOR = "/"; public static final String ROOT_PATH = PATH_SEPARATOR; @@ -116,7 +125,14 @@ public class OCFile implements Parcelable, Comparable, ServerFileInterfa private long e2eCounter = -1; @Nullable private GeoLocation geolocation; - private List tags = new ArrayList<>(); + private List tags = new ArrayList<>(); + private Long internalFolderSyncTimestamp = -1L; + private String internalFolderSyncResult = ""; + + // region Recommend files variables + private boolean recommendedFile = false; + private String reason = ""; + // endregion /** * URI to the local path of the file contents, if stored in the device; cached after first call to @@ -157,6 +173,7 @@ public class OCFile implements Parcelable, Comparable, ServerFileInterfa fileId = source.readLong(); parentId = source.readLong(); fileLength = source.readLong(); + uploadTimestamp = source.readLong(); creationTimestamp = source.readLong(); modificationTimestamp = source.readLong(); modificationTimestampAtLastSyncForData = source.readLong(); @@ -202,6 +219,7 @@ public class OCFile implements Parcelable, Comparable, ServerFileInterfa dest.writeLong(fileId); dest.writeLong(parentId); dest.writeLong(fileLength); + dest.writeLong(uploadTimestamp); dest.writeLong(creationTimestamp); dest.writeLong(modificationTimestamp); dest.writeLong(modificationTimestampAtLastSyncForData); @@ -380,26 +398,11 @@ public class OCFile implements Parcelable, Comparable, ServerFileInterfa return localUri; } - - public Uri getLegacyExposedFileUri() { - if (TextUtils.isEmpty(localPath)) { - return null; - } - - if (exposedFileUri == null) { - return Uri.parse(ContentResolver.SCHEME_FILE + "://" + WebdavUtils.encodePath(localPath)); - } - - return exposedFileUri; - - } - /* - Partly disabled because not all apps understand paths that we get via this method for now - */ public Uri getExposedFileUri(Context context) { if (TextUtils.isEmpty(localPath)) { return null; } + if (exposedFileUri == null) { try { exposedFileUri = FileProvider.getUriForFile( @@ -407,9 +410,7 @@ public class OCFile implements Parcelable, Comparable, ServerFileInterfa context.getString(R.string.file_provider_authority), new File(localPath)); } catch (IllegalArgumentException ex) { - // Could not share file using FileProvider URI scheme. - // Fall back to legacy URI parsing. - getLegacyExposedFileUri(); + Log_OC.d(TAG, "Given File is outside the paths supported by the provider"); } } @@ -496,6 +497,7 @@ public class OCFile implements Parcelable, Comparable, ServerFileInterfa localPath = null; mimeType = null; fileLength = 0; + uploadTimestamp = 0; creationTimestamp = 0; modificationTimestamp = 0; modificationTimestampAtLastSyncForData = 0; @@ -577,7 +579,7 @@ public class OCFile implements Parcelable, Comparable, ServerFileInterfa @Override public int hashCode() { - return 31 * (int) (fileId ^ (fileId >>> 32)) + (int) (parentId ^ (parentId >>> 32)); + return Objects.hash(fileId,parentId); } @NonNull @@ -639,27 +641,67 @@ public class OCFile implements Parcelable, Comparable, ServerFileInterfa } public boolean isSharedWithMe() { - String permissions = getPermissions(); - return permissions != null && permissions.contains(PERMISSION_SHARED_WITH_ME); + return hasPermission(PERMISSION_SHARED); } public boolean canReshare() { - String permissions = getPermissions(); - return permissions != null && permissions.contains(PERMISSION_CAN_RESHARE); + return hasPermission(PERMISSION_CAN_RESHARE); + } + + public boolean canCreateFileAndFolder() { + return hasPermission(PERMISSION_CAN_CREATE_FILE_AND_FOLDER); + } + + public boolean mounted() { + return hasPermission(PERMISSION_MOUNTED); + } + + public boolean canRead() { + return hasPermission(PERMISSION_CAN_READ); + } + + public boolean canCreateFileInsideFolder() { + return hasPermission(PERMISSION_CAN_CREATE_FILE_INSIDE_FOLDER); + } + + public boolean canCreateFolderInsideFolder() { + return hasPermission(PERMISSION_CAN_CREATE_FOLDER_INSIDE_FOLDER); + } + + /** + * Determines whether the current account has the ability to delete the file or leave the share. + * + *

+ * - If the file is shared with the current account (i.e., the user is the recipient), + * the user cannot delete the file itself but can leave the shared file. + *

+ * - If the file is belongs to the current user. User can delete the file. + * + * @return true if the user is allowed to either delete or leave the share; false otherwise. + */ + public boolean canDeleteOrLeaveShare() { + return hasPermission(PERMISSION_CAN_DELETE_OR_LEAVE_SHARE); + } + + public boolean canRename() { + return hasPermission(PERMISSION_CAN_RENAME); } public boolean canWrite() { - String permissions = getPermissions(); - return permissions != null && permissions.contains(PERMISSION_CAN_WRITE); + return hasPermission(PERMISSION_CAN_WRITE); } - public boolean isGroupFolder() { + public boolean canMove() { + return hasPermission(PERMISSION_CAN_MOVE); + } + + private boolean hasPermission(String permission) { String permissions = getPermissions(); - return permissions != null && permissions.contains(PERMISSION_GROUPFOLDER); + return permissions != null && permissions.contains(permission); } public Integer getFileOverlayIconId(boolean isAutoUploadFolder) { - if (WebdavEntry.MountType.GROUP == mountType || isGroupFolder()) { + if (WebdavEntry.MountType.GROUP == mountType || mounted()) { return R.drawable.ic_folder_overlay_account_group; } else if (sharedViaLink && !encrypted) { return R.drawable.ic_folder_overlay_link; @@ -678,7 +720,7 @@ public class OCFile implements Parcelable, Comparable, ServerFileInterfa } } - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public static final Parcelable.Creator CREATOR = new Parcelable.Creator<>() { @Override public OCFile createFromParcel(Parcel source) { @@ -706,6 +748,10 @@ public class OCFile implements Parcelable, Comparable, ServerFileInterfa return this.fileLength; } + public boolean isFileEligibleForImmediatePreview() { + return fileLength <= MAX_FILE_SIZE_FOR_IMMEDIATE_PREVIEW_BYTES; + } + public long getCreationTimestamp() { return this.creationTimestamp; } @@ -717,10 +763,6 @@ public class OCFile implements Parcelable, Comparable, ServerFileInterfa return this.modificationTimestamp; } - public long getUploadTimestamp() { - return this.uploadTimestamp; - } - public long getModificationTimestampAtLastSyncForData() { return this.modificationTimestampAtLastSyncForData; } @@ -753,10 +795,18 @@ public class OCFile implements Parcelable, Comparable, ServerFileInterfa return this.etagOnServer; } + public boolean isEtagChanged() { + return !StringExtensionsKt.isNotBlankAndEquals(getEtag(), getEtagOnServer()); + } + public boolean isSharedViaLink() { return this.sharedViaLink; } + public boolean isShared() { + return isSharedViaLink() || isSharedWithSharee() || isSharedWithMe() || !sharees.isEmpty(); + } + public String getPermissions() { return this.permissions; } @@ -773,6 +823,14 @@ public class OCFile implements Parcelable, Comparable, ServerFileInterfa return this.downloading; } + public boolean isRootDirectory() { + return ROOT_PATH.equals(decryptedRemotePath); + } + + public boolean isOfflineOperation() { + return getRemoteId() == null; + } + public String getEtagInConflict() { return this.etagInConflict; } @@ -1032,11 +1090,11 @@ public class OCFile implements Parcelable, Comparable, ServerFileInterfa return geolocation; } - public List getTags() { + public List getTags() { return tags; } - public void setTags(List tags) { + public void setTags(List tags) { this.tags = tags; } @@ -1045,18 +1103,67 @@ public class OCFile implements Parcelable, Comparable, ServerFileInterfa } public void setE2eCounter(@Nullable Long e2eCounter) { - if (e2eCounter == null) { - this.e2eCounter = -1; - } else { - this.e2eCounter = e2eCounter; + this.e2eCounter = Objects.requireNonNullElse(e2eCounter, -1L); + } + + public boolean isInternalFolderSync() { + if (internalFolderSyncTimestamp == null) { + return false; } + + return internalFolderSyncTimestamp >= 0; + } + + public Long getInternalFolderSyncTimestamp() { + return Objects.requireNonNullElse(internalFolderSyncTimestamp, -1L); + } + + public void setInternalFolderSyncTimestamp(Long internalFolderSyncTimestamp) { + this.internalFolderSyncTimestamp = internalFolderSyncTimestamp; + } + + public String getInternalFolderSyncResult() { + return internalFolderSyncResult; + } + + public void setInternalFolderSyncResult(String internalFolderSyncResult) { + this.internalFolderSyncResult = internalFolderSyncResult; } public boolean isAPKorAAB() { - if ("gplay".equals(BuildConfig.FLAVOR)) { + if (BuildHelper.INSTANCE.isFlavourGPlay()) { return getFileName().endsWith(".apk") || getFileName().endsWith(".aab"); } else { return false; } } + + public long getUploadTimestamp() { + return uploadTimestamp; + } + + public void setUploadTimestamp(long uploadTimestamp) { + this.uploadTimestamp = uploadTimestamp; + } + + public boolean exists() { + final String storagePath = getStoragePath(); + return storagePath != null && new File(storagePath).exists(); + } + + public void setReason(String value) { + reason = value; + } + + public String getReason() { + return reason; + } + + public void setIsRecommendedFile(boolean value) { + recommendedFile = value; + } + + public boolean isRecommendedFile() { + return recommendedFile; + } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/PushConfigurationState.java b/app/src/main/java/com/owncloud/android/datamodel/PushConfigurationState.java index d24989f..617f703 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/PushConfigurationState.java +++ b/app/src/main/java/com/owncloud/android/datamodel/PushConfigurationState.java @@ -6,7 +6,7 @@ * Copyright (C) 2017 Mario Danic * Copyright (C) 2018 Andy Scherzinger * - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel; diff --git a/app/src/main/java/com/owncloud/android/datamodel/QuickPermissionModel.kt b/app/src/main/java/com/owncloud/android/datamodel/QuickPermissionModel.kt deleted file mode 100644 index 8116311..0000000 --- a/app/src/main/java/com/owncloud/android/datamodel/QuickPermissionModel.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author TSI-mc - * Copyright (C) 2021 TSI-mc - * Copyright (C) 2021 Nextcloud GmbH - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.owncloud.android.datamodel - -data class QuickPermissionModel(val permissionName: String, val isSelected: Boolean) diff --git a/app/src/main/java/com/owncloud/android/datamodel/ReceiverFlag.kt b/app/src/main/java/com/owncloud/android/datamodel/ReceiverFlag.kt index 100d726..b64be49 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/ReceiverFlag.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/ReceiverFlag.kt @@ -1,9 +1,9 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel @@ -15,7 +15,5 @@ enum class ReceiverFlag { NotExported; @RequiresApi(Build.VERSION_CODES.TIRAMISU) - fun getId(): Int { - return Context.RECEIVER_NOT_EXPORTED - } + fun getId(): Int = Context.RECEIVER_NOT_EXPORTED } diff --git a/app/src/main/java/com/owncloud/android/datamodel/SharesType.kt b/app/src/main/java/com/owncloud/android/datamodel/SharesType.kt new file mode 100644 index 0000000..fb58a83 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/datamodel/SharesType.kt @@ -0,0 +1,13 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Tobias Kaminsky + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.datamodel + +enum class SharesType { + INTERNAL, + EXTERNAL +} diff --git a/app/src/main/java/com/owncloud/android/datamodel/SignatureVerification.kt b/app/src/main/java/com/owncloud/android/datamodel/SignatureVerification.kt index 96f3b41..a021746 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/SignatureVerification.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/SignatureVerification.kt @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2022 Unpublished - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel diff --git a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolder.java b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolder.java index 3e5b8fa..257edc8 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolder.java +++ b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolder.java @@ -5,14 +5,19 @@ * Copyright (C) 2016 Tobias Kaminsky * Copyright (C) 2016 Nextcloud * - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel; +import com.nextcloud.client.device.PowerManagementService; +import com.nextcloud.client.network.ConnectivityService; import com.nextcloud.client.preferences.SubFolderRule; +import com.nextcloud.utils.extensions.SyncedFolderExtensionsKt; import com.owncloud.android.files.services.NameCollisionPolicy; +import com.owncloud.android.utils.MimeTypeUtil; +import java.io.File; import java.io.Serializable; /** @@ -103,7 +108,7 @@ public class SyncedFolder implements Serializable, Cloneable { * * @param id id */ - protected SyncedFolder(long id, + public SyncedFolder(long id, String localPath, String remotePath, boolean wifiOnly, @@ -174,6 +179,28 @@ public class SyncedFolder implements Serializable, Cloneable { return this.chargingOnly; } + /** + * Indicates whether the "Also upload existing files" option is enabled for this folder. + * + *

+ * This flag controls how files in the folder are treated when auto-upload is enabled: + *

    + *
  • If {@code true} (existing files are included): + *
      + *
    • All files in the folder, regardless of creation date, will be uploaded.
    • + *
    + *
  • + *
  • If {@code false} (existing files are skipped): + *
      + *
    • Only files created or added after the folder was enabled will be uploaded.
    • + *
    • Files that existed before enabling will be skipped, based on their creation time.
    • + *
    + *
  • + *
+ *

+ * + * @return {@code true} if existing files should also be uploaded, {@code false} otherwise + */ public boolean isExisting() { return this.existing; } @@ -274,11 +301,20 @@ public class SyncedFolder implements Serializable, Cloneable { this.excludeHidden = excludeHidden; } - public boolean containsFile(String filePath){ - return filePath.contains(localPath); + public boolean containsTypedFile(File file,String filePath){ + boolean isCorrectMediaType = + (getType() == MediaFolderType.IMAGE && MimeTypeUtil.isImage(file)) || + (getType() == MediaFolderType.VIDEO && MimeTypeUtil.isVideo(file)) || + getType() == MediaFolderType.CUSTOM; + return filePath.contains(localPath) && isCorrectMediaType; } public long getLastScanTimestampMs() { return lastScanTimestampMs; } public void setLastScanTimestampMs(long lastScanTimestampMs) { this.lastScanTimestampMs = lastScanTimestampMs; } + + public long getTotalScanInterval(ConnectivityService connectivityService, PowerManagementService powerManagementService) { + final var calculatedScanInterval = SyncedFolderExtensionsKt.calculateScanInterval(this, connectivityService, powerManagementService); + return lastScanTimestampMs + calculatedScanInterval.getFirst(); + } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java index 2bef272..cbbf445 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java +++ b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2016 Andy Scherzinger * SPDX-FileCopyrightText: 2016 Nextcloud - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel; diff --git a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java index 5147889..ff4c00c 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java +++ b/app/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2016 Andy Scherzinger * SPDX-FileCopyrightText: 2016 Nextcloud - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel; @@ -15,9 +15,14 @@ import android.net.Uri; import com.nextcloud.client.account.User; import com.nextcloud.client.core.Clock; +import com.nextcloud.client.database.NextcloudDatabase; +import com.nextcloud.client.database.dao.SyncedFolderDao; +import com.nextcloud.client.database.entity.SyncedFolderEntity; +import com.nextcloud.client.database.entity.SyncedFolderEntityKt; import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.client.preferences.AppPreferencesImpl; import com.nextcloud.client.preferences.SubFolderRule; +import com.owncloud.android.MainApp; import com.owncloud.android.db.ProviderMeta; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.files.model.ServerFileInterface; @@ -27,6 +32,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Observable; +import javax.annotation.Nullable; + import androidx.annotation.NonNull; import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR; @@ -40,6 +47,7 @@ public class SyncedFolderProvider extends Observable { private final ContentResolver mContentResolver; private final AppPreferences preferences; private final Clock clock; + public final SyncedFolderDao dao = NextcloudDatabase.getInstance(MainApp.getAppContext()).syncedFolderDao(); /** * constructor. @@ -181,25 +189,26 @@ public class SyncedFolderProvider extends Observable { } public SyncedFolder findByLocalPathAndAccount(String localPath, User user) { + final SyncedFolderEntity entity = dao.findByLocalPathAndAccount(localPath, user.getAccountName()); + if (entity == null) { + return null; + } + return SyncedFolderEntityKt.toSyncedFolder(entity); + } + + @Nullable + public SyncedFolder getSyncedFolderByID(Long syncedFolderID) { SyncedFolder result = null; Cursor cursor = mContentResolver.query( ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS, null, - ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH + " LIKE ? AND " + - ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ACCOUNT + " =? ", - new String[]{localPath + "%", user.getAccountName()}, + ProviderMeta.ProviderTableMeta._ID + " =? ", + new String[]{syncedFolderID.toString()}, null - ); + ); - if (cursor != null && cursor.getCount() == 1) { + if (cursor != null && cursor.getCount() == 1 && cursor.moveToFirst()) { result = createSyncedFolderFromCursor(cursor); - } else { - if (cursor == null) { - Log_OC.e(TAG, "Sync folder db cursor for local path=" + localPath + " in NULL."); - } else { - Log_OC.e(TAG, cursor.getCount() + " items for local path=" + localPath - + " available in sync folder db. Expected 1. Failed to update sync folder db."); - } } if (cursor != null) { @@ -228,12 +237,12 @@ public class SyncedFolderProvider extends Observable { * * @param id for the synced folder. */ - private int deleteSyncFolderWithId(long id) { - return mContentResolver.delete( - ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS, - ProviderMeta.ProviderTableMeta._ID + " = ?", - new String[]{String.valueOf(id)} - ); + private void deleteSyncFolderWithId(long id) { + mContentResolver.delete( + ProviderMeta.ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS, + ProviderMeta.ProviderTableMeta._ID + " = ?", + new String[]{String.valueOf(id)} + ); } @@ -444,9 +453,6 @@ public class SyncedFolderProvider extends Observable { } else { if (cursor == null) { Log_OC.e(TAG, "Sync folder db cursor for remote path = " + remotePath + " in NULL."); - } else { - Log_OC.e(TAG, cursor.getCount() + " items for remote path = " + remotePath - + " available in sync folder db. Expected 1 or greater than 1. Failed to update sync folder db."); } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/Template.kt b/app/src/main/java/com/owncloud/android/datamodel/Template.kt index b345256..3f3beb6 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/Template.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/Template.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2018 Tobias Kaminsky * SPDX-FileCopyrightText: 2018 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel @@ -14,15 +14,13 @@ import kotlinx.parcelize.Parcelize * Template for creating a file from it via RichDocuments app */ @Parcelize -data class Template( - val id: Long, - val name: String, - val thumbnailLink: String, - val type: Type, - val extension: String -) : Parcelable { +data class Template(val id: Long, val name: String, val thumbnailLink: String, val type: Type, val extension: String) : + Parcelable { enum class Type { - DOCUMENT, SPREADSHEET, PRESENTATION, UNKNOWN; + DOCUMENT, + SPREADSHEET, + PRESENTATION, + UNKNOWN; companion object { @JvmStatic diff --git a/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java b/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java index f4b62ac..4cea79a 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java @@ -7,10 +7,11 @@ * SPDX-FileCopyrightText: 2019 Chris Narkiewicz * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2014 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.datamodel; +import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; @@ -36,6 +37,7 @@ import android.widget.ImageView; import com.nextcloud.client.account.User; import com.nextcloud.client.network.ConnectivityService; +import com.nextcloud.utils.BitmapExtensionsKt; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.lib.common.OwnCloudAccount; @@ -62,17 +64,22 @@ import org.apache.commons.httpclient.methods.GetMethod; import java.io.File; import java.io.FileNotFoundException; -import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import static com.nextcloud.utils.extensions.ThumbnailsCacheManagerExtensionsKt.getExifOrientation; + /** * Manager for concurrent access to thumbnails cache. */ @@ -98,6 +105,8 @@ public final class ThumbnailsCacheManager { private static final CompressFormat mCompressFormat = CompressFormat.JPEG; private static final int mCompressQuality = 70; private static OwnCloudClient mClient; + private static final int THUMBNAIL_SIZE_IN_KB = 512; + private static final int RESIZED_IMAGE_SIZE_IN_KB = 10240; public static final Bitmap mDefaultImg = BitmapFactory.decodeResource(MainApp.getAppContext().getResources(), R.drawable.file_image); @@ -108,9 +117,10 @@ public final class ThumbnailsCacheManager { private ThumbnailsCacheManager() { } - public static class InitDiskCacheTask extends AsyncTask { - @Override - protected Void doInBackground(File... params) { + private static final ExecutorService executor = Executors.newSingleThreadExecutor(); + + public static void initDiskCacheAsync() { + executor.execute(() -> { synchronized (mThumbnailsDiskCacheLock) { mThumbnailCacheStarting = true; @@ -147,8 +157,7 @@ public final class ThumbnailsCacheManager { mThumbnailCacheStarting = false; // Finished initialization mThumbnailsDiskCacheLock.notifyAll(); // Wake any waiting threads } - return null; - } + }); } /** @@ -188,7 +197,8 @@ public final class ThumbnailsCacheManager { Bitmap thumbnail = ThumbnailUtils.extractThumbnail(bitmap, pxW, pxH); // Rotate image, obeying exif tag - thumbnail = BitmapUtils.rotateImage(thumbnail,path); + int orientation = getExifOrientation(path); + thumbnail = BitmapExtensionsKt.rotateBitmapViaExif(thumbnail, orientation); // Add thumbnail to cache // do not overwrite any pre-existing image @@ -199,11 +209,55 @@ public final class ThumbnailsCacheManager { return thumbnail; } + public static void removeFromCache(@Nullable OCFile file) { + if (file == null) { + return; + } + + final var keys = new String[] { PREFIX_RESIZED_IMAGE + file.getRemoteId(), PREFIX_THUMBNAIL + file.getRemoteId() }; + + synchronized (mThumbnailsDiskCacheLock) { + if (mThumbnailCache == null) { + return; + } + + for (String key: keys) { + mThumbnailCache.removeKey(key); + } + } + } + public static void addBitmapToCache(String key, Bitmap bitmap) { synchronized (mThumbnailsDiskCacheLock) { - if (mThumbnailCache != null) { - mThumbnailCache.put(key, bitmap); + if (mThumbnailCache == null) { + return; } + + // Check if the bitmap is already cached + Bitmap cachedBitmap = mThumbnailCache.getBitmap(key); + if (cachedBitmap == null) { + cachedBitmap = mThumbnailCache.getScaledBitmap(key, bitmap.getWidth(), bitmap.getHeight()); + } + + if (cachedBitmap != null && BitmapExtensionsKt.allocationKilobyte(cachedBitmap) <= THUMBNAIL_SIZE_IN_KB) { + Log_OC.d(TAG, "Cached version is already within size limits, no need to scale: " + key); + return; + } + + // do not scale down resized images + int size; + if (key.startsWith("r")) { + size = RESIZED_IMAGE_SIZE_IN_KB; + } else { + size = THUMBNAIL_SIZE_IN_KB; + } + + if (BitmapExtensionsKt.allocationKilobyte(bitmap) > size) { + Log_OC.d(TAG, "Scaling bitmap before caching: " + key); + bitmap = BitmapExtensionsKt.scaleUntil(bitmap, size); + } + + mThumbnailCache.put(key, bitmap); } } @@ -250,10 +304,10 @@ public final class ThumbnailsCacheManager { private final FileDataStorageManager storageManager; private final WeakReference imageViewReference; private OCFile file; - private String imageKey; + private final String imageKey; private GalleryListener listener; - private List asyncTasks; - private int backgroundColor; + private final List asyncTasks; + private final int backgroundColor; private boolean newImage = false; public GalleryImageGenerationTask( @@ -262,8 +316,7 @@ public final class ThumbnailsCacheManager { FileDataStorageManager storageManager, List asyncTasks, String imageKey, - int backgroundColor - ) { + int backgroundColor) { this.user = user; this.storageManager = storageManager; imageViewReference = new WeakReference<>(imageView); @@ -272,72 +325,98 @@ public final class ThumbnailsCacheManager { this.backgroundColor = backgroundColor; } - public void setListener(GalleryImageGenerationTask.GalleryListener listener) { + public void setListener(GalleryListener listener) { this.listener = listener; } - public String getImageKey() { - return imageKey; - } - @Override protected Bitmap doInBackground(Object... params) { Bitmap thumbnail; - file = (OCFile) params[0]; - - - if (file.getRemoteId() != null && file.isPreviewAvailable()) { - // Thumbnail in cache? - thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( - ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.getRemoteId() - ); - - if (thumbnail != null && !file.isUpdateThumbnailNeeded()) { - Float size = (float) ThumbnailsCacheManager.getThumbnailDimension(); - - // resized dimensions - ImageDimension imageDimension = file.getImageDimension(); - if (imageDimension == null || - imageDimension.getWidth() != size || - imageDimension.getHeight() != size) { - file.setImageDimension(new ImageDimension(thumbnail.getWidth(), thumbnail.getHeight())); - storageManager.saveFile(file); - } - - if (MimeTypeUtil.isVideo(file)) { - return ThumbnailsCacheManager.addVideoOverlay(thumbnail, MainApp.getAppContext()); - } else { - return thumbnail; - } - } else { - try { - mClient = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(user.toOwnCloudAccount(), - MainApp.getAppContext()); - - thumbnail = doResizedImageInBackground(file, storageManager); - newImage = true; - - if (MimeTypeUtil.isVideo(file) && thumbnail != null) { - thumbnail = addVideoOverlay(thumbnail, MainApp.getAppContext()); - } - - } catch (OutOfMemoryError oome) { - Log_OC.e(TAG, "Out of memory"); - } catch (Throwable t) { - // the app should never break due to a problem with thumbnails - Log_OC.e(TAG, "Generation of gallery image for " + file + " failed", t); - } - - return thumbnail; - } + if (params == null || params.length == 0 || !(params[0] instanceof OCFile)) { + Log_OC.d(TAG, "Downloaded file is null or is not an instance of OCFile"); + return null; } + file = (OCFile) params[0]; + + if (file.getRemoteId() != null || file.isPreviewAvailable()) { + // Thumbnail in cache? + thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( + ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + file.getRemoteId()); + + if (thumbnail != null && !file.isUpdateThumbnailNeeded()) + return getThumbnailFromCache(thumbnail); + + return getThumbnailFromServerAndAddToCache(thumbnail); + } + + Log_OC.d(TAG, "File cannot be previewed"); return null; } + @Nullable + private Bitmap getThumbnailFromServerAndAddToCache(Bitmap thumbnail) { + try { + mClient = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(user.toOwnCloudAccount(), + MainApp.getAppContext()); + + thumbnail = doResizedImageInBackground(file, storageManager); + newImage = true; + + if (MimeTypeUtil.isVideo(file) && thumbnail != null) { + thumbnail = addVideoOverlay(thumbnail, MainApp.getAppContext()); + } + + } catch (OutOfMemoryError oome) { + Log_OC.e(TAG, "Out of memory"); + } catch (Throwable t) { + // the app should never break due to a problem with thumbnails + Log_OC.e(TAG, "Generation of gallery image for " + file + " failed", t); + } + + return thumbnail; + } + + private Bitmap getThumbnailFromCache(Bitmap thumbnail) { + float size = (float) ThumbnailsCacheManager.getThumbnailDimension(); + + // resized dimensions + ImageDimension imageDimension = file.getImageDimension(); + if (imageDimension == null || + imageDimension.getWidth() != size || + imageDimension.getHeight() != size) { + file.setImageDimension(new ImageDimension(thumbnail.getWidth(), thumbnail.getHeight())); + storageManager.saveFile(file); + } + + Bitmap result = thumbnail; + if (MimeTypeUtil.isVideo(file)) { + result = ThumbnailsCacheManager.addVideoOverlay(thumbnail, MainApp.getAppContext()); + } + + if (BitmapExtensionsKt.allocationKilobyte(thumbnail) > THUMBNAIL_SIZE_IN_KB) { + result = getScaledThumbnailAfterSave(result); + } + + return result; + } + + private Bitmap getScaledThumbnailAfterSave(Bitmap thumbnail) { + Bitmap result = BitmapExtensionsKt.scaleUntil(thumbnail, THUMBNAIL_SIZE_IN_KB); + + synchronized (mThumbnailsDiskCacheLock) { + if (mThumbnailCache != null) { + Log_OC.d(TAG, "Scaling bitmap before caching: " + imageKey); + mThumbnailCache.put(imageKey, result); + } + } + + return result; + } + protected void onPostExecute(Bitmap bitmap) { - if (bitmap != null && imageViewReference != null) { + if (bitmap != null && imageViewReference.get() != null) { final ImageView imageView = imageViewReference.get(); final GalleryImageGenerationTask bitmapWorkerTask = getGalleryImageGenerationTask(imageView); @@ -434,7 +513,7 @@ public final class ThumbnailsCacheManager { } protected void onPostExecute(Bitmap bitmap) { - if (imageViewReference != null) { + if (imageViewReference.get() != null) { final ImageView imageView = imageViewReference.get(); final FrameLayout frameLayout = frameLayoutReference.get(); @@ -462,7 +541,7 @@ public final class ThumbnailsCacheManager { } } else { if (fileFragment instanceof PreviewImageFragment) { - ((PreviewImageFragment) fileFragment).setErrorPreviewMessage(); + ((PreviewImageFragment) fileFragment).handleUnsupportedImage(); } } }).start(); @@ -657,8 +736,7 @@ public final class ThumbnailsCacheManager { int pxH; pxW = pxH = getThumbnailDimension(); - if (file instanceof OCFile) { - OCFile ocFile = (OCFile) file; + if (file instanceof OCFile ocFile) { if (ocFile.isDown()) { Bitmap bitmap; if (MimeTypeUtil.isVideo(ocFile)) { @@ -761,19 +839,14 @@ public final class ThumbnailsCacheManager { private int getThumbnailDimension() { // Converts dp to pixel Resources r = MainApp.getAppContext().getResources(); - Double d = Math.pow(2, Math.floor(Math.log(r.getDimension(R.dimen.file_icon_size_grid)) / Math.log(2))); - return d.intValue(); + double d = Math.pow(2, Math.floor(Math.log(r.getDimension(R.dimen.file_icon_size_grid)) / Math.log(2))); + return (int) d; } private Bitmap doFileInBackground() { File file = (File)mFile; - final String imageKey; - if (mImageKey != null) { - imageKey = mImageKey; - } else { - imageKey = String.valueOf(file.hashCode()); - } + final String imageKey = Objects.requireNonNullElseGet(mImageKey, () -> String.valueOf(file.hashCode())); // local file should always generate a thumbnail mImageKey = PREFIX_THUMBNAIL + mImageKey; @@ -812,7 +885,7 @@ public final class ThumbnailsCacheManager { private final WeakReference mImageViewReference; private File mFile; private String mImageKey; - private final Context mContext; + @SuppressLint("StaticFieldLeak") private final Context mContext; private final ViewThemeUtils viewThemeUtils; public MediaThumbnailGenerationTask(ImageView imageView, @@ -885,13 +958,7 @@ public final class ThumbnailsCacheManager { } private Bitmap doFileInBackground(File file, Type type) { - final String imageKey; - - if (mImageKey != null) { - imageKey = mImageKey; - } else { - imageKey = String.valueOf(file.hashCode()); - } + final String imageKey = Objects.requireNonNullElseGet(mImageKey, () -> String.valueOf(file.hashCode())); // Check disk cache in background thread Bitmap thumbnail = getBitmapFromDiskCache(imageKey); @@ -908,20 +975,12 @@ public final class ThumbnailsCacheManager { thumbnail = addThumbnailToCache(imageKey, bitmap, file.getPath(), px, px); } } else if (Type.VIDEO == type) { - MediaMetadataRetriever retriever = new MediaMetadataRetriever(); - try { + try (MediaMetadataRetriever retriever = new MediaMetadataRetriever()) { retriever.setDataSource(file.getAbsolutePath()); thumbnail = retriever.getFrameAtTime(-1); } catch (Exception ex) { // can't create a bitmap Log_OC.w(TAG, "Failed to create bitmap from video " + file.getAbsolutePath()); - } finally { - try { - retriever.release(); - } catch (RuntimeException | IOException ex) { - // Ignore failure at this point. - Log_OC.w(TAG, "Failed release MediaMetadataRetriever for " + file.getAbsolutePath()); - } } if (thumbnail != null) { @@ -951,7 +1010,7 @@ public final class ThumbnailsCacheManager { private final String mUserId; private final String displayName; private final String mServerName; - private final Context mContext; + @SuppressLint("StaticFieldLeak") private final Context mContext; public AvatarGenerationTask(AvatarGenerationListener avatarGenerationListener, @@ -1004,17 +1063,6 @@ public final class ThumbnailsCacheManager { } } - /** - * Converts size of file icon from dp to pixel - * - * @return int - */ - private int getAvatarDimension() { - // Converts dp to pixel - Resources r = MainApp.getAppContext().getResources(); - return Math.round(r.getDimension(R.dimen.file_avatar_size)); - } - private @NonNull Drawable doAvatarInBackground() { Bitmap avatar; @@ -1108,7 +1156,7 @@ public final class ThumbnailsCacheManager { try { return TextDrawable.createAvatarByUserId(displayName, mAvatarRadius); } catch (Exception e1) { - return ResourcesCompat.getDrawable(mResources, R.drawable.ic_user, null); + return ResourcesCompat.getDrawable(mResources, R.drawable.ic_user_outline, null); } } else { return BitmapUtils.bitmapToCircularBitmapDrawable(mResources, avatar); @@ -1138,8 +1186,7 @@ public final class ThumbnailsCacheManager { public static ThumbnailGenerationTask getBitmapWorkerTask(ImageView imageView) { if (imageView != null) { final Drawable drawable = imageView.getDrawable(); - if (drawable instanceof AsyncThumbnailDrawable) { - final AsyncThumbnailDrawable asyncDrawable = (AsyncThumbnailDrawable) drawable; + if (drawable instanceof AsyncThumbnailDrawable asyncDrawable) { return asyncDrawable.getBitmapWorkerTask(); } } @@ -1149,8 +1196,7 @@ public final class ThumbnailsCacheManager { private static ResizedImageGenerationTask getResizedImageGenerationWorkerTask(ImageView imageView) { if (imageView != null) { final Drawable drawable = imageView.getDrawable(); - if (drawable instanceof AsyncResizedImageDrawable) { - final AsyncResizedImageDrawable asyncDrawable = (AsyncResizedImageDrawable) drawable; + if (drawable instanceof AsyncResizedImageDrawable asyncDrawable) { return asyncDrawable.getBitmapWorkerTask(); } } @@ -1160,8 +1206,7 @@ public final class ThumbnailsCacheManager { private static GalleryImageGenerationTask getGalleryImageGenerationTask(ImageView imageView) { if (imageView != null) { final Drawable drawable = imageView.getDrawable(); - if (drawable instanceof AsyncGalleryImageDrawable) { - final AsyncGalleryImageDrawable asyncDrawable = (AsyncGalleryImageDrawable) drawable; + if (drawable instanceof AsyncGalleryImageDrawable asyncDrawable) { return asyncDrawable.getBitmapWorkerTask(); } } @@ -1248,9 +1293,11 @@ public final class ThumbnailsCacheManager { } /** - * adapted from https://stackoverflow.com/a/8113368 + * adapted from ... */ private static Bitmap handlePNG(Bitmap source, int newWidth, int newHeight) { + Bitmap softwareBitmap = source.copy(Bitmap.Config.ARGB_8888, false); + int sourceWidth = source.getWidth(); int sourceHeight = source.getHeight(); @@ -1269,8 +1316,9 @@ public final class ThumbnailsCacheManager { Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(dest); - canvas.drawColor(MainApp.getAppContext().getResources().getColor(R.color.background_color_png)); - canvas.drawBitmap(source, null, targetRect, null); + int color = ContextCompat.getColor(MainApp.getAppContext(),R.color.background_color_png); + canvas.drawColor(color); + canvas.drawBitmap(softwareBitmap, null, targetRect, null); return dest; } @@ -1388,7 +1436,7 @@ public final class ThumbnailsCacheManager { GetMethod getMethod = null; try { String uri = mClient.getBaseUri() + "/index.php/core/preview?fileId=" - + file.getRemoteId() + + file.getLocalId() + "&x=" + (pxW / 2) + "&y=" + (pxH / 2) + "&a=1&mode=cover&forceIcon=0"; Log_OC.d(TAG, "generate resized image: " + file.getFileName() + " URI: " + uri); getMethod = new GetMethod(uri); @@ -1422,9 +1470,10 @@ public final class ThumbnailsCacheManager { } } - // resized dimensions + // resized dimensions and set update thumbnail needed to false to prevent rendering loop if (thumbnail != null) { file.setImageDimension(new ImageDimension(thumbnail.getWidth(), thumbnail.getHeight())); + file.setUpdateThumbnailNeeded(false); storageManager.saveFile(file); } } diff --git a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java index 51143ec..9a72022 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2024 Jonas Mayer * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2018-2020 Tobias Kaminsky @@ -10,23 +11,23 @@ * SPDX-FileCopyrightText: 2016 María Asensio Valverde * SPDX-FileCopyrightText: 2016 David A. Velasco * SPDX-FileCopyrightText: 2014 Luke Owncloud - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.datamodel; -import android.content.ContentProviderOperation; -import android.content.ContentProviderResult; import android.content.ContentResolver; import android.content.ContentValues; -import android.content.OperationApplicationException; import android.database.Cursor; -import android.net.Uri; -import android.os.RemoteException; import com.nextcloud.client.account.CurrentAccountProvider; import com.nextcloud.client.account.User; +import com.nextcloud.client.database.NextcloudDatabase; +import com.nextcloud.client.database.dao.UploadDao; +import com.nextcloud.client.database.entity.UploadEntity; +import com.nextcloud.client.database.entity.UploadEntityKt; import com.nextcloud.client.jobs.upload.FileUploadHelper; import com.nextcloud.client.jobs.upload.FileUploadWorker; +import com.nextcloud.utils.autoRename.AutoRename; import com.owncloud.android.MainApp; import com.owncloud.android.db.OCUpload; import com.owncloud.android.db.ProviderMeta.ProviderTableMeta; @@ -34,7 +35,9 @@ import com.owncloud.android.db.UploadResult; import com.owncloud.android.files.services.NameCollisionPolicy; import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.common.utils.Log_OC; +import com.owncloud.android.lib.resources.status.OCCapability; import com.owncloud.android.operations.UploadFileOperation; +import com.owncloud.android.utils.theme.CapabilityUtils; import java.io.File; import java.util.ArrayList; @@ -54,13 +57,19 @@ import androidx.annotation.VisibleForTesting; public class UploadsStorageManager extends Observable { private static final String TAG = UploadsStorageManager.class.getSimpleName(); + private static final String IS_EQUAL = "== ?"; + private static final String EQUAL = "=="; + private static final String OR = " OR "; private static final String AND = " AND "; + private static final String ANGLE_BRACKETS = "<>"; private static final int SINGLE_RESULT = 1; private static final long QUERY_PAGE_SIZE = 100; private final ContentResolver contentResolver; private final CurrentAccountProvider currentAccountProvider; + private OCCapability capability; + public final UploadDao uploadDao = NextcloudDatabase.getInstance(MainApp.getAppContext()).uploadDao(); public UploadsStorageManager( CurrentAccountProvider currentAccountProvider, @@ -73,111 +82,36 @@ public class UploadsStorageManager extends Observable { this.currentAccountProvider = currentAccountProvider; } - /** - * Stores an upload object in DB. - * - * @param ocUpload Upload object to store - * @return upload id, -1 if the insert process fails. - */ - public long storeUpload(OCUpload ocUpload) { - OCUpload existingUpload = getPendingCurrentOrFailedUpload(ocUpload); - if (existingUpload != null) { - Log_OC.v(TAG, "Will update upload in db since " + ocUpload.getLocalPath() + " already exists as " + - "pending, current or failed upload"); - long existingId = existingUpload.getUploadId(); - ocUpload.setUploadId(existingId); - updateUpload(ocUpload); - return existingId; - } - - - Log_OC.v(TAG, "Inserting " + ocUpload.getLocalPath() + " with status=" + ocUpload.getUploadStatus()); - - ContentValues cv = getContentValues(ocUpload); - Uri result = getDB().insert(ProviderTableMeta.CONTENT_URI_UPLOADS, cv); - - Log_OC.d(TAG, "storeUpload returns with: " + result + " for file: " + ocUpload.getLocalPath()); - if (result == null) { - Log_OC.e(TAG, "Failed to insert item " + ocUpload.getLocalPath() + " into upload db."); - return -1; - } else { - long new_id = Long.parseLong(result.getPathSegments().get(1)); - ocUpload.setUploadId(new_id); - notifyObserversNow(); - - return new_id; - } - - } - - public long[] storeUploads(final List ocUploads) { - Log_OC.v(TAG, "Inserting " + ocUploads.size() + " uploads"); - ArrayList operations = new ArrayList<>(ocUploads.size()); - for (OCUpload ocUpload : ocUploads) { - - OCUpload existingUpload = getPendingCurrentOrFailedUpload(ocUpload); - if (existingUpload != null) { - Log_OC.v(TAG, "Will update upload in db since " + ocUpload.getLocalPath() + " already exists as" + - " pending, current or failed upload"); - ocUpload.setUploadId(existingUpload.getUploadId()); - updateUpload(ocUpload); - continue; - } - - final ContentProviderOperation operation = ContentProviderOperation - .newInsert(ProviderTableMeta.CONTENT_URI_UPLOADS) - .withValues(getContentValues(ocUpload)) - .build(); - operations.add(operation); - } - + private void initOCCapability() { try { - final ContentProviderResult[] contentProviderResults = getDB().applyBatch(MainApp.getAuthority(), operations); - final long[] newIds = new long[ocUploads.size()]; - for (int i = 0; i < contentProviderResults.length; i++) { - final ContentProviderResult result = contentProviderResults[i]; - final long new_id = Long.parseLong(result.uri.getPathSegments().get(1)); - ocUploads.get(i).setUploadId(new_id); - newIds[i] = new_id; - } - notifyObserversNow(); - return newIds; - } catch (OperationApplicationException | RemoteException e) { - Log_OC.e(TAG, "Error inserting uploads", e); + this.capability = CapabilityUtils.getCapability(MainApp.getAppContext()); + } catch (RuntimeException e) { + Log_OC.e(TAG,"Failed to set OCCapability: Dependencies are not yet ready."); } - - return null; } - @NonNull - private ContentValues getContentValues(OCUpload ocUpload) { - ContentValues cv = new ContentValues(); - cv.put(ProviderTableMeta.UPLOADS_LOCAL_PATH, ocUpload.getLocalPath()); - cv.put(ProviderTableMeta.UPLOADS_REMOTE_PATH, ocUpload.getRemotePath()); - cv.put(ProviderTableMeta.UPLOADS_ACCOUNT_NAME, ocUpload.getAccountName()); - cv.put(ProviderTableMeta.UPLOADS_FILE_SIZE, ocUpload.getFileSize()); - cv.put(ProviderTableMeta.UPLOADS_STATUS, ocUpload.getUploadStatus().value); - cv.put(ProviderTableMeta.UPLOADS_LOCAL_BEHAVIOUR, ocUpload.getLocalAction()); - cv.put(ProviderTableMeta.UPLOADS_NAME_COLLISION_POLICY, ocUpload.getNameCollisionPolicy().serialize()); - cv.put(ProviderTableMeta.UPLOADS_IS_CREATE_REMOTE_FOLDER, ocUpload.isCreateRemoteFolder() ? 1 : 0); - cv.put(ProviderTableMeta.UPLOADS_LAST_RESULT, ocUpload.getLastResult().getValue()); - cv.put(ProviderTableMeta.UPLOADS_CREATED_BY, ocUpload.getCreatedBy()); - cv.put(ProviderTableMeta.UPLOADS_IS_WHILE_CHARGING_ONLY, ocUpload.isWhileChargingOnly() ? 1 : 0); - cv.put(ProviderTableMeta.UPLOADS_IS_WIFI_ONLY, ocUpload.isUseWifiOnly() ? 1 : 0); - cv.put(ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN, ocUpload.getFolderUnlockToken()); - return cv; - } - - /** * Update an upload object in DB. * * @param ocUpload Upload object with state to update * @return num of updated uploads. */ - public int updateUpload(OCUpload ocUpload) { + public synchronized int updateUpload(OCUpload ocUpload) { Log_OC.v(TAG, "Updating " + ocUpload.getLocalPath() + " with status=" + ocUpload.getUploadStatus()); + OCUpload existingUpload = getUploadById(ocUpload.getUploadId()); + if (existingUpload == null) { + Log_OC.e(TAG, "Upload not found for ID: " + ocUpload.getUploadId()); + return 0; + } + + if (!existingUpload.getAccountName().equals(ocUpload.getAccountName())) { + Log_OC.e(TAG, "Account mismatch for upload ID " + ocUpload.getUploadId() + + ": expected " + existingUpload.getAccountName() + + ", got " + ocUpload.getAccountName()); + return 0; + } + ContentValues cv = new ContentValues(); cv.put(ProviderTableMeta.UPLOADS_LOCAL_PATH, ocUpload.getLocalPath()); cv.put(ProviderTableMeta.UPLOADS_REMOTE_PATH, ocUpload.getRemotePath()); @@ -190,8 +124,8 @@ public class UploadsStorageManager extends Observable { int result = getDB().update(ProviderTableMeta.CONTENT_URI_UPLOADS, cv, - ProviderTableMeta._ID + "=?", - new String[]{String.valueOf(ocUpload.getUploadId())} + ProviderTableMeta._ID + "=? AND " + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "=?", + new String[]{String.valueOf(ocUpload.getUploadId()), ocUpload.getAccountName()} ); Log_OC.d(TAG, "updateUpload returns with: " + result + " for file: " + ocUpload.getLocalPath()); @@ -247,11 +181,10 @@ public class UploadsStorageManager extends Observable { * @param localPath path of the file to upload in the device storage * @return 1 if file status was updated, else 0. */ - private int updateUploadStatus(long id, UploadStatus status, UploadResult result, String remotePath, + private void updateUploadStatus(long id, UploadStatus status, UploadResult result, String remotePath, String localPath) { //Log_OC.v(TAG, "Updating "+filepath+" with uploadStatus="+status +" and result="+result); - int returnValue = 0; Cursor c = getDB().query( ProviderTableMeta.CONTENT_URI_UPLOADS, null, @@ -265,14 +198,13 @@ public class UploadsStorageManager extends Observable { Log_OC.e(TAG, c.getCount() + " items for id=" + id + " available in UploadDb. Expected 1. Failed to update upload db."); } else { - returnValue = updateUploadInternal(c, status, result, remotePath, localPath); + updateUploadInternal(c, status, result, remotePath, localPath); } c.close(); } else { Log_OC.e(TAG, "Cursor is null"); } - return returnValue; } /** @@ -360,52 +292,6 @@ public class UploadsStorageManager extends Observable { return getUploads(null, (String[]) null); } - public OCUpload getPendingCurrentOrFailedUpload(OCUpload upload) { - try (Cursor cursor = getDB().query( - ProviderTableMeta.CONTENT_URI_UPLOADS, - null, - ProviderTableMeta.UPLOADS_REMOTE_PATH + "=? and " + - ProviderTableMeta.UPLOADS_LOCAL_PATH + "=? and " + - ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "=? and (" + - ProviderTableMeta.UPLOADS_STATUS + "=? or " + - ProviderTableMeta.UPLOADS_STATUS + "=? )", - new String[]{ - upload.getRemotePath(), - upload.getLocalPath(), - upload.getAccountName(), - String.valueOf(UploadStatus.UPLOAD_IN_PROGRESS.value), - String.valueOf(UploadStatus.UPLOAD_FAILED.value) - }, - ProviderTableMeta.UPLOADS_REMOTE_PATH + " ASC")) { - - if (cursor != null) { - if (cursor.moveToFirst()) { - return createOCUploadFromCursor(cursor); - } - } - } - return null; - } - - public OCUpload getUploadByRemotePath(String remotePath) { - OCUpload result = null; - try (Cursor cursor = getDB().query( - ProviderTableMeta.CONTENT_URI_UPLOADS, - null, - ProviderTableMeta.UPLOADS_REMOTE_PATH + "=?", - new String[]{remotePath}, - ProviderTableMeta.UPLOADS_REMOTE_PATH + " ASC")) { - - if (cursor != null) { - if (cursor.moveToFirst()) { - result = createOCUploadFromCursor(cursor); - } - } - } - Log_OC.d(TAG, "Retrieve job " + result + " for remote path " + remotePath); - return result; - } - public @Nullable OCUpload getUploadById(long id) { OCUpload result = null; @@ -425,6 +311,20 @@ public class UploadsStorageManager extends Observable { return result; } + public List getUploadsByIds(long[] uploadIds, String accountName) { + final List result = new ArrayList<>(); + + final List entities = uploadDao.getUploadsByIds(uploadIds, accountName); + entities.forEach(uploadEntity -> { + OCUpload ocUpload = createOCUploadFromEntity(uploadEntity); + if (ocUpload != null) { + result.add(ocUpload); + } + }); + + return result; + } + private OCUpload[] getUploads(@Nullable String selection, @Nullable String... selectionArgs) { final List uploads = new ArrayList<>(); long page = 0; @@ -464,11 +364,11 @@ public class UploadsStorageManager extends Observable { @NonNull private List getUploadPage(final long afterId, @Nullable String selection, @Nullable String... selectionArgs) { - return getUploadPage(afterId, true, selection, selectionArgs); + return getUploadPage(QUERY_PAGE_SIZE, afterId, true, selection, selectionArgs); } @NonNull - private List getUploadPage(final long afterId, final boolean descending, @Nullable String selection, @Nullable String... selectionArgs) { + private List getUploadPage(long limit, final long afterId, final boolean descending, @Nullable String selection, @Nullable String... selectionArgs) { List uploads = new ArrayList<>(); String pageSelection = selection; String[] pageSelectionArgs = selectionArgs; @@ -499,13 +399,20 @@ public class UploadsStorageManager extends Observable { } else { Log_OC.d(TAG, String.format(Locale.ENGLISH, "QUERY: %s ROWID: %d", selection, afterId)); } + + String sortOrder; + if (limit > 0) { + sortOrder = String.format(Locale.ENGLISH, "_id " + sortDirection + " LIMIT %d", limit); + } else { + sortOrder = String.format(Locale.ENGLISH, "_id " + sortDirection); + } + Cursor c = getDB().query( ProviderTableMeta.CONTENT_URI_UPLOADS, null, pageSelection, pageSelectionArgs, - String.format(Locale.ENGLISH, "_id " + sortDirection + " LIMIT %d", QUERY_PAGE_SIZE) - ); + sortOrder); if (c != null) { if (c.moveToFirst()) { @@ -523,11 +430,27 @@ public class UploadsStorageManager extends Observable { return uploads; } + @Nullable + private OCUpload createOCUploadFromEntity(UploadEntity entity) { + if (entity == null) { + return null; + } + initOCCapability(); + return UploadEntityKt.toOCUpload(entity, capability); + } + private OCUpload createOCUploadFromCursor(Cursor c) { + initOCCapability(); + OCUpload upload = null; if (c != null) { String localPath = c.getString(c.getColumnIndexOrThrow(ProviderTableMeta.UPLOADS_LOCAL_PATH)); + String remotePath = c.getString(c.getColumnIndexOrThrow(ProviderTableMeta.UPLOADS_REMOTE_PATH)); + if (capability != null) { + remotePath = AutoRename.INSTANCE.rename(remotePath, capability); + } + String accountName = c.getString(c.getColumnIndexOrThrow(ProviderTableMeta.UPLOADS_ACCOUNT_NAME)); upload = new OCUpload(localPath, remotePath, accountName); @@ -553,157 +476,49 @@ public class UploadsStorageManager extends Observable { return upload; } - public OCUpload[] getCurrentAndPendingUploadsForCurrentAccount() { - User user = currentAccountProvider.getUser(); - - return getCurrentAndPendingUploadsForAccount(user.getAccountName()); - } - - public OCUpload[] getCurrentAndPendingUploadsForAccount(final @NonNull String accountName) { - return getUploads("( " + ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_IN_PROGRESS.value + - " OR " + ProviderTableMeta.UPLOADS_LAST_RESULT + - "==" + UploadResult.DELAYED_FOR_WIFI.getValue() + - " OR " + ProviderTableMeta.UPLOADS_LAST_RESULT + - "==" + UploadResult.LOCK_FAILED.getValue() + - " OR " + ProviderTableMeta.UPLOADS_LAST_RESULT + - "==" + UploadResult.DELAYED_FOR_CHARGING.getValue() + - " OR " + ProviderTableMeta.UPLOADS_LAST_RESULT + - "==" + UploadResult.DELAYED_IN_POWER_SAVE_MODE.getValue() + - " ) AND " + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", - accountName); - } - - /** - * Gets a page of uploads after afterId, where uploads are sorted by ascending upload id. - *

- * If afterId is -1, returns the first page - */ - public List getCurrentAndPendingUploadsForAccountPageAscById(final long afterId, final @NonNull String accountName) { - final String selection = "( " + ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_IN_PROGRESS.value + - " OR " + ProviderTableMeta.UPLOADS_LAST_RESULT + - "==" + UploadResult.DELAYED_FOR_WIFI.getValue() + - " OR " + ProviderTableMeta.UPLOADS_LAST_RESULT + - "==" + UploadResult.LOCK_FAILED.getValue() + - " OR " + ProviderTableMeta.UPLOADS_LAST_RESULT + - "==" + UploadResult.DELAYED_FOR_CHARGING.getValue() + - " OR " + ProviderTableMeta.UPLOADS_LAST_RESULT + - "==" + UploadResult.DELAYED_IN_POWER_SAVE_MODE.getValue() + - " ) AND " + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?"; - return getUploadPage(afterId, false, selection, accountName); - } - - /** - * Get all failed uploads. - */ - public OCUpload[] getFailedUploads() { - return getUploads("(" + ProviderTableMeta.UPLOADS_STATUS + "== ?" + - " OR " + ProviderTableMeta.UPLOADS_LAST_RESULT + - "==" + UploadResult.DELAYED_FOR_WIFI.getValue() + - " OR " + ProviderTableMeta.UPLOADS_LAST_RESULT + - "==" + UploadResult.LOCK_FAILED.getValue() + - " OR " + ProviderTableMeta.UPLOADS_LAST_RESULT + - "==" + UploadResult.DELAYED_FOR_CHARGING.getValue() + - " OR " + ProviderTableMeta.UPLOADS_LAST_RESULT + - "==" + UploadResult.DELAYED_IN_POWER_SAVE_MODE.getValue() + - " ) AND " + ProviderTableMeta.UPLOADS_LAST_RESULT + - "!= " + UploadResult.VIRUS_DETECTED.getValue() - , String.valueOf(UploadStatus.UPLOAD_FAILED.value)); + public long[] getCurrentUploadIds(final @NonNull String accountName) { + final var result = uploadDao.getAllIds(UploadStatus.UPLOAD_IN_PROGRESS.value, accountName); + return result.stream() + .mapToLong(Integer::longValue) + .toArray(); } public OCUpload[] getUploadsForAccount(final @NonNull String accountName) { - return getUploads(ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", accountName); - } - - public OCUpload[] getFinishedUploadsForCurrentAccount() { - User user = currentAccountProvider.getUser(); - - return getUploads(ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_SUCCEEDED.value + AND + - ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", user.getAccountName()); - } - - public OCUpload[] getCancelledUploadsForCurrentAccount() { - User user = currentAccountProvider.getUser(); - - return getUploads(ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_CANCELLED.value + AND + - ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", user.getAccountName()); - } - - /** - * Get all uploads which where successfully completed. - */ - public OCUpload[] getFinishedUploads() { - - return getUploads(ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_SUCCEEDED.value, (String[]) null); - } - - public OCUpload[] getFailedButNotDelayedUploadsForCurrentAccount() { - User user = currentAccountProvider.getUser(); - - return getUploads(ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value + - AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() + - AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - "<>" + UploadResult.LOCK_FAILED.getValue() + - AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - "<>" + UploadResult.DELAYED_FOR_CHARGING.getValue() + - AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - "<>" + UploadResult.DELAYED_IN_POWER_SAVE_MODE.getValue() + - AND + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", - user.getAccountName()); - } - - /** - * Get all failed uploads, except for those that were not performed due to lack of Wifi connection. - * - * @return Array of failed uploads, except for those that were not performed due to lack of Wifi connection. - */ - public OCUpload[] getFailedButNotDelayedUploads() { - - return getUploads(ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value + AND + - ProviderTableMeta.UPLOADS_LAST_RESULT + "<>" + UploadResult.LOCK_FAILED.getValue() + - AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() + - AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - "<>" + UploadResult.DELAYED_FOR_CHARGING.getValue() + - AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - "<>" + UploadResult.DELAYED_IN_POWER_SAVE_MODE.getValue(), - (String[]) null - ); + return getUploads(ProviderTableMeta.UPLOADS_ACCOUNT_NAME + IS_EQUAL, accountName); } private ContentResolver getDB() { return contentResolver; } - public long clearFailedButNotDelayedUploads() { + public void clearFailedButNotDelayedUploads() { User user = currentAccountProvider.getUser(); final long deleted = getDB().delete( ProviderTableMeta.CONTENT_URI_UPLOADS, - ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_FAILED.value + + ProviderTableMeta.UPLOADS_STATUS + EQUAL + UploadStatus.UPLOAD_FAILED.value + AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - "<>" + UploadResult.LOCK_FAILED.getValue() + + ANGLE_BRACKETS + UploadResult.LOCK_FAILED.getValue() + AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - "<>" + UploadResult.DELAYED_FOR_WIFI.getValue() + + ANGLE_BRACKETS + UploadResult.DELAYED_FOR_WIFI.getValue() + AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - "<>" + UploadResult.DELAYED_FOR_CHARGING.getValue() + + ANGLE_BRACKETS + UploadResult.DELAYED_FOR_CHARGING.getValue() + AND + ProviderTableMeta.UPLOADS_LAST_RESULT + - "<>" + UploadResult.DELAYED_IN_POWER_SAVE_MODE.getValue() + - AND + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", + ANGLE_BRACKETS + UploadResult.DELAYED_IN_POWER_SAVE_MODE.getValue() + + AND + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + IS_EQUAL, new String[]{user.getAccountName()} ); Log_OC.d(TAG, "delete all failed uploads but those delayed for Wifi"); if (deleted > 0) { notifyObserversNow(); } - return deleted; } public void clearCancelledUploadsForCurrentAccount() { User user = currentAccountProvider.getUser(); final long deleted = getDB().delete( ProviderTableMeta.CONTENT_URI_UPLOADS, - ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_CANCELLED.value + AND + - ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", new String[]{user.getAccountName()} + ProviderTableMeta.UPLOADS_STATUS + EQUAL + UploadStatus.UPLOAD_CANCELLED.value + AND + + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + IS_EQUAL, new String[]{user.getAccountName()} ); Log_OC.d(TAG, "delete all cancelled uploads"); @@ -712,19 +527,18 @@ public class UploadsStorageManager extends Observable { } } - public long clearSuccessfulUploads() { + public void clearSuccessfulUploads() { User user = currentAccountProvider.getUser(); final long deleted = getDB().delete( ProviderTableMeta.CONTENT_URI_UPLOADS, - ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_SUCCEEDED.value + AND + - ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", new String[]{user.getAccountName()} + ProviderTableMeta.UPLOADS_STATUS + EQUAL + UploadStatus.UPLOAD_SUCCEEDED.value + AND + + ProviderTableMeta.UPLOADS_ACCOUNT_NAME + IS_EQUAL, new String[]{user.getAccountName()} ); Log_OC.d(TAG, "delete all successful uploads"); if (deleted > 0) { notifyObserversNow(); } - return deleted; } /** @@ -770,7 +584,7 @@ public class UploadsStorageManager extends Observable { upload.getRemotePath(), localPath ); - } else { + } else if (uploadResult.getCode() != RemoteOperationResult.ResultCode.USER_CANCELLED){ updateUploadStatus( upload.getOCUploadId(), UploadStatus.UPLOAD_FAILED, @@ -798,43 +612,10 @@ public class UploadsStorageManager extends Observable { ); } - /** - * Changes the status of any in progress upload from UploadStatus.UPLOAD_IN_PROGRESS to UploadStatus.UPLOAD_FAILED - * - * @return Number of uploads which status was changed. - */ - public int failInProgressUploads(UploadResult fail) { - Log_OC.v(TAG, "Updating state of any killed upload"); - - ContentValues cv = new ContentValues(); - cv.put(ProviderTableMeta.UPLOADS_STATUS, UploadStatus.UPLOAD_FAILED.getValue()); - cv.put( - ProviderTableMeta.UPLOADS_LAST_RESULT, - fail != null ? fail.getValue() : UploadResult.UNKNOWN.getValue() - ); - cv.put(ProviderTableMeta.UPLOADS_UPLOAD_END_TIMESTAMP, Calendar.getInstance().getTimeInMillis()); - - int result = getDB().update( - ProviderTableMeta.CONTENT_URI_UPLOADS, - cv, - ProviderTableMeta.UPLOADS_STATUS + "=?", - new String[]{String.valueOf(UploadStatus.UPLOAD_IN_PROGRESS.getValue())} - ); - - if (result == 0) { - Log_OC.v(TAG, "No upload was killed"); - } else { - Log_OC.w(TAG, Integer.toString(result) + " uploads where abruptly interrupted"); - notifyObserversNow(); - } - - return result; - } - @VisibleForTesting - public int removeAllUploads() { + public void removeAllUploads() { Log_OC.v(TAG, "Delete all uploads!"); - return getDB().delete( + getDB().delete( ProviderTableMeta.CONTENT_URI_UPLOADS, "", new String[]{}); @@ -877,17 +658,13 @@ public class UploadsStorageManager extends Observable { } public static UploadStatus fromValue(int value) { - switch (value) { - case 0: - return UPLOAD_IN_PROGRESS; - case 1: - return UPLOAD_FAILED; - case 2: - return UPLOAD_SUCCEEDED; - case 3: - return UPLOAD_CANCELLED; - } - return null; + return switch (value) { + case 0 -> UPLOAD_IN_PROGRESS; + case 1 -> UPLOAD_FAILED; + case 2 -> UPLOAD_SUCCEEDED; + case 3 -> UPLOAD_CANCELLED; + default -> null; + }; } public int getValue() { diff --git a/app/src/main/java/com/owncloud/android/datamodel/VirtualFolderType.java b/app/src/main/java/com/owncloud/android/datamodel/VirtualFolderType.java index a867615..1259b70 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/VirtualFolderType.java +++ b/app/src/main/java/com/owncloud/android/datamodel/VirtualFolderType.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2017 Tobias Kaminsky * SPDX-FileCopyrightText: 2017 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel; diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Data.java b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Data.java index 422f2d7..da3ad49 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Data.java +++ b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Data.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel.e2e.v1.decrypted; diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedFile.java b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedFile.java index a2e8660..dcd2d78 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedFile.java +++ b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedFile.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel.e2e.v1.decrypted; diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedFolderMetadataFileV1.java b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedFolderMetadataFileV1.java index d362022..c80021f 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedFolderMetadataFileV1.java +++ b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedFolderMetadataFileV1.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel.e2e.v1.decrypted; diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedMetadata.java b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedMetadata.java index 8b65475..2d2036e 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedMetadata.java +++ b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/DecryptedMetadata.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel.e2e.v1.decrypted; diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Encrypted.java b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Encrypted.java index 5a5e4fa..3d566c0 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Encrypted.java +++ b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Encrypted.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel.e2e.v1.decrypted; diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Sharing.java b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Sharing.java index 94d2d3b..6bb5b89 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Sharing.java +++ b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/decrypted/Sharing.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel.e2e.v1.decrypted; diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/encrypted/EncryptedFile.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/encrypted/EncryptedFile.kt index 7e7905a..63a9573 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/encrypted/EncryptedFile.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/encrypted/EncryptedFile.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel.e2e.v1.encrypted diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/encrypted/EncryptedFolderMetadataFileV1.java b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/encrypted/EncryptedFolderMetadataFileV1.java index eb814d4..17d8eae 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/encrypted/EncryptedFolderMetadataFileV1.java +++ b/app/src/main/java/com/owncloud/android/datamodel/e2e/v1/encrypted/EncryptedFolderMetadataFileV1.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel.e2e.v1.encrypted; diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFile.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFile.kt index c7d619a..406f859 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFile.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFile.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel.e2e.v2.decrypted diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFolderMetadataFile.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFolderMetadataFile.kt index 6c6b1e3..c092b8d 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFolderMetadataFile.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedFolderMetadataFile.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel.e2e.v2.decrypted diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedMetadata.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedMetadata.kt index 88099b6..7401571 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedMetadata.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedMetadata.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel.e2e.v2.decrypted diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedUser.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedUser.kt index 31352d6..ac02b66 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedUser.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/decrypted/DecryptedUser.kt @@ -3,11 +3,8 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel.e2e.v2.decrypted -data class DecryptedUser( - val userId: String, - val certificate: String -) +data class DecryptedUser(val userId: String, val certificate: String, var decryptedMetadataKey: String?) diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFiledrop.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFiledrop.kt index 801e04a..3f36adf 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFiledrop.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFiledrop.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel.e2e.v2.encrypted diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFiledropUser.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFiledropUser.kt index a8c0998..bbe3e1c 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFiledropUser.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFiledropUser.kt @@ -3,11 +3,8 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel.e2e.v2.encrypted -data class EncryptedFiledropUser( - val userId: String, - val encryptedFiledropKey: String -) +data class EncryptedFiledropUser(val userId: String, val encryptedFiledropKey: String) diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFolderMetadataFile.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFolderMetadataFile.kt index 1a1644a..5c7b279 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFolderMetadataFile.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedFolderMetadataFile.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel.e2e.v2.encrypted diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedMetadata.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedMetadata.kt index 981c666..c060aa7 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedMetadata.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedMetadata.kt @@ -3,12 +3,8 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel.e2e.v2.encrypted -data class EncryptedMetadata( - val ciphertext: String, - val nonce: String, - val authenticationTag: String -) +data class EncryptedMetadata(val ciphertext: String, val nonce: String, val authenticationTag: String) diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedUser.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedUser.kt index 757353d..97a2c0a 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedUser.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/EncryptedUser.kt @@ -3,12 +3,8 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel.e2e.v2.encrypted -data class EncryptedUser( - val userId: String, - val certificate: String, - val encryptedMetadataKey: String -) +data class EncryptedUser(val userId: String, val certificate: String, val encryptedMetadataKey: String) diff --git a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/FiledropData.kt b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/FiledropData.kt index 6c92b42..bbefb6f 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/FiledropData.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/e2e/v2/encrypted/FiledropData.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datamodel.e2e.v2.encrypted diff --git a/app/src/main/java/com/owncloud/android/datamodel/quickPermission/QuickPermission.kt b/app/src/main/java/com/owncloud/android/datamodel/quickPermission/QuickPermission.kt new file mode 100644 index 0000000..460edeb --- /dev/null +++ b/app/src/main/java/com/owncloud/android/datamodel/quickPermission/QuickPermission.kt @@ -0,0 +1,10 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.datamodel.quickPermission + +data class QuickPermission(val type: QuickPermissionType, var isSelected: Boolean) diff --git a/app/src/main/java/com/owncloud/android/datamodel/quickPermission/QuickPermissionType.kt b/app/src/main/java/com/owncloud/android/datamodel/quickPermission/QuickPermissionType.kt new file mode 100644 index 0000000..a09aa06 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/datamodel/quickPermission/QuickPermissionType.kt @@ -0,0 +1,51 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.datamodel.quickPermission + +import android.content.Context +import android.graphics.drawable.Drawable +import androidx.core.content.ContextCompat +import com.owncloud.android.R +import com.owncloud.android.lib.resources.shares.OCShare + +enum class QuickPermissionType(val iconId: Int, val textId: Int) { + NONE(R.drawable.ic_unknown, R.string.unknown), + VIEW_ONLY(R.drawable.ic_eye, R.string.share_permission_view_only), + CAN_EDIT(R.drawable.ic_edit, R.string.share_permission_can_edit), + FILE_REQUEST(R.drawable.ic_file_request, R.string.share_permission_file_request), + SECURE_FILE_DROP(R.drawable.ic_file_request, R.string.share_permission_secure_file_drop), + CUSTOM_PERMISSIONS(R.drawable.ic_custom_permissions, R.string.share_custom_permission); + + fun getText(context: Context): String = context.getString(textId) + + fun getIcon(context: Context): Drawable? = ContextCompat.getDrawable(context, iconId) + + fun getPermissionFlag(isFolder: Boolean): Int = when (this) { + NONE -> OCShare.NO_PERMISSION + VIEW_ONLY -> OCShare.READ_PERMISSION_FLAG + CAN_EDIT -> if (isFolder) OCShare.MAXIMUM_PERMISSIONS_FOR_FOLDER else OCShare.MAXIMUM_PERMISSIONS_FOR_FILE + FILE_REQUEST -> OCShare.CREATE_PERMISSION_FLAG + SECURE_FILE_DROP -> OCShare.CREATE_PERMISSION_FLAG + OCShare.READ_PERMISSION_FLAG + else -> { + // Custom permission's flag can't be determined + OCShare.NO_PERMISSION + } + } + + fun getAvailablePermissions(hasFileRequestPermission: Boolean): List { + val permissions = listOf(VIEW_ONLY, CAN_EDIT, FILE_REQUEST, CUSTOM_PERMISSIONS) + val result = if (hasFileRequestPermission) permissions else permissions.filter { it != FILE_REQUEST } + + return result.map { type -> + QuickPermission( + type = type, + isSelected = (type == this) + ) + } + } +} diff --git a/app/src/main/java/com/owncloud/android/datastorage/DataStorageProvider.java b/app/src/main/java/com/owncloud/android/datastorage/DataStorageProvider.java index cada61b..deb7568 100644 --- a/app/src/main/java/com/owncloud/android/datastorage/DataStorageProvider.java +++ b/app/src/main/java/com/owncloud/android/datastorage/DataStorageProvider.java @@ -5,7 +5,7 @@ * Copyright (C) 2016 Nextcloud * Copyright (C) 2016 Bartosz Przybylski * - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.datastorage; diff --git a/app/src/main/java/com/owncloud/android/datastorage/StoragePoint.java b/app/src/main/java/com/owncloud/android/datastorage/StoragePoint.java index 4516e53..8fa7d83 100644 --- a/app/src/main/java/com/owncloud/android/datastorage/StoragePoint.java +++ b/app/src/main/java/com/owncloud/android/datastorage/StoragePoint.java @@ -5,7 +5,7 @@ * Copyright (C) 2016 Nextcloud * Copyright (C) 2016 Bartosz Przybylski * - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.datastorage; diff --git a/app/src/main/java/com/owncloud/android/datastorage/UniqueStorageList.java b/app/src/main/java/com/owncloud/android/datastorage/UniqueStorageList.java index 2319490..c421e55 100644 --- a/app/src/main/java/com/owncloud/android/datastorage/UniqueStorageList.java +++ b/app/src/main/java/com/owncloud/android/datastorage/UniqueStorageList.java @@ -5,7 +5,7 @@ * Copyright (C) 2016 Nextcloud * Copyright (C) 2016 Bartosz Przybylski * - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.datastorage; diff --git a/app/src/main/java/com/owncloud/android/datastorage/providers/AbstractCommandLineStoragePoint.java b/app/src/main/java/com/owncloud/android/datastorage/providers/AbstractCommandLineStoragePoint.java index 4e0d494..66dcb12 100644 --- a/app/src/main/java/com/owncloud/android/datastorage/providers/AbstractCommandLineStoragePoint.java +++ b/app/src/main/java/com/owncloud/android/datastorage/providers/AbstractCommandLineStoragePoint.java @@ -5,7 +5,7 @@ * Copyright (C) 2016 Nextcloud * Copyright (C) 2016 Bartosz Przybylski * - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.datastorage.providers; @@ -44,7 +44,7 @@ abstract class AbstractCommandLineStoragePoint extends AbstractStoragePointProvi process.waitFor(); final InputStream is = process.getInputStream(); - final byte buffer[] = new byte[1024]; + final byte[] buffer = new byte[1024]; while (is.read(buffer) != -1) { s.append(new String(buffer, "UTF8")); } diff --git a/app/src/main/java/com/owncloud/android/datastorage/providers/AbstractStoragePointProvider.java b/app/src/main/java/com/owncloud/android/datastorage/providers/AbstractStoragePointProvider.java index cce5b45..bc9b9ff 100644 --- a/app/src/main/java/com/owncloud/android/datastorage/providers/AbstractStoragePointProvider.java +++ b/app/src/main/java/com/owncloud/android/datastorage/providers/AbstractStoragePointProvider.java @@ -5,7 +5,7 @@ * Copyright (C) 2016 Nextcloud * Copyright (C) 2016 Bartosz Przybylski * - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.datastorage.providers; diff --git a/app/src/main/java/com/owncloud/android/datastorage/providers/EnvironmentStoragePointProvider.java b/app/src/main/java/com/owncloud/android/datastorage/providers/EnvironmentStoragePointProvider.java index 3842bb6..868dee9 100644 --- a/app/src/main/java/com/owncloud/android/datastorage/providers/EnvironmentStoragePointProvider.java +++ b/app/src/main/java/com/owncloud/android/datastorage/providers/EnvironmentStoragePointProvider.java @@ -5,7 +5,7 @@ * Copyright (C) 2016 Nextcloud * Copyright (C) 2016 Bartosz Przybylski * - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.datastorage.providers; diff --git a/app/src/main/java/com/owncloud/android/datastorage/providers/HardcodedStoragePointProvider.java b/app/src/main/java/com/owncloud/android/datastorage/providers/HardcodedStoragePointProvider.java index d847757..dcd6026 100644 --- a/app/src/main/java/com/owncloud/android/datastorage/providers/HardcodedStoragePointProvider.java +++ b/app/src/main/java/com/owncloud/android/datastorage/providers/HardcodedStoragePointProvider.java @@ -5,7 +5,7 @@ * Copyright (C) 2016 Nextcloud * Copyright (C) 2016 Bartosz Przybylski * - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.datastorage.providers; diff --git a/app/src/main/java/com/owncloud/android/datastorage/providers/IStoragePointProvider.java b/app/src/main/java/com/owncloud/android/datastorage/providers/IStoragePointProvider.java index ed261c1..5f8f38b 100644 --- a/app/src/main/java/com/owncloud/android/datastorage/providers/IStoragePointProvider.java +++ b/app/src/main/java/com/owncloud/android/datastorage/providers/IStoragePointProvider.java @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2016-2018 Andy Scherzinger * SPDX-FileCopyrightText: 2016 Bartosz Przybylski * - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.datastorage.providers; diff --git a/app/src/main/java/com/owncloud/android/datastorage/providers/MountCommandStoragePointProvider.java b/app/src/main/java/com/owncloud/android/datastorage/providers/MountCommandStoragePointProvider.java index 0b28da4..2cfc52c 100644 --- a/app/src/main/java/com/owncloud/android/datastorage/providers/MountCommandStoragePointProvider.java +++ b/app/src/main/java/com/owncloud/android/datastorage/providers/MountCommandStoragePointProvider.java @@ -5,7 +5,7 @@ * Copyright (C) 2016 Nextcloud * Copyright (C) 2016 Bartosz Przybylski * - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.datastorage.providers; @@ -49,7 +49,7 @@ public class MountCommandStoragePointProvider extends AbstractCommandLineStorage for (String line : mounted.split("\n")) { if (!line.toLowerCase(Locale.US).contains("asec") && sPattern.matcher(line).matches()) { - String parts[] = line.split(" "); + String[] parts = line.split(" "); for (String path : parts) { if (path.length() > 0 && path.charAt(0) == '/' && !path.toLowerCase(Locale.US).contains("vold")) { result.add(path); diff --git a/app/src/main/java/com/owncloud/android/datastorage/providers/SystemDefaultStoragePointProvider.java b/app/src/main/java/com/owncloud/android/datastorage/providers/SystemDefaultStoragePointProvider.java index 7914914..7706a77 100644 --- a/app/src/main/java/com/owncloud/android/datastorage/providers/SystemDefaultStoragePointProvider.java +++ b/app/src/main/java/com/owncloud/android/datastorage/providers/SystemDefaultStoragePointProvider.java @@ -5,7 +5,7 @@ * Copyright (C) 2016 Nextcloud * Copyright (C) 2016 Bartosz Przybylski * - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.datastorage.providers; diff --git a/app/src/main/java/com/owncloud/android/datastorage/providers/VDCStoragePointProvider.java b/app/src/main/java/com/owncloud/android/datastorage/providers/VDCStoragePointProvider.java index b497862..f2d6959 100644 --- a/app/src/main/java/com/owncloud/android/datastorage/providers/VDCStoragePointProvider.java +++ b/app/src/main/java/com/owncloud/android/datastorage/providers/VDCStoragePointProvider.java @@ -5,7 +5,7 @@ * Copyright (C) 2016 Nextcloud * Copyright (C) 2016 Bartosz Przybylski * - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.datastorage.providers; @@ -28,11 +28,8 @@ public class VDCStoragePointProvider extends AbstractCommandLineStoragePoint { @Override public List getAvailableStoragePoint() { - List result = new Vector<>(); - result.addAll(getPaths(getCommandLineResult())); - - return result; + return new Vector<>(getPaths(getCommandLineResult())); } @Override @@ -44,7 +41,7 @@ public class VDCStoragePointProvider extends AbstractCommandLineStoragePoint { List result = new Vector<>(); for (String line : vdcResources.split("\n")) { - String vdcLine[] = line.split(" "); + String[] vdcLine = line.split(" "); try { int status = Integer.parseInt(vdcLine[0]); if (status != sVDCVolumeList) { diff --git a/app/src/main/java/com/owncloud/android/db/OCUpload.java b/app/src/main/java/com/owncloud/android/db/OCUpload.java index 2cf40cf..167e943 100644 --- a/app/src/main/java/com/owncloud/android/db/OCUpload.java +++ b/app/src/main/java/com/owncloud/android/db/OCUpload.java @@ -10,7 +10,7 @@ * SPDX-FileCopyrightText: 2014 Luke Owncloud * SPDX-FileCopyrightText: 2015-2016 David A. Velasco * SPDX-FileCopyrightText: 2015-2016 María Asensio Valverde - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.db; @@ -238,7 +238,7 @@ public class OCUpload implements Parcelable { /**** * */ - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public static final Parcelable.Creator CREATOR = new Parcelable.Creator<>() { @Override public OCUpload createFromParcel(Parcel source) { @@ -292,10 +292,9 @@ public class OCUpload implements Parcelable { @SuppressFBWarnings("SEO_SUBOPTIMAL_EXPRESSION_ORDER") public boolean isSame(@Nullable Object obj) { - if (!(obj instanceof OCUpload)) { + if (!(obj instanceof OCUpload other)) { return false; } - OCUpload other = (OCUpload) obj; return this.uploadId == other.uploadId && localPath.equals(other.localPath) && remotePath.equals(other.remotePath) && diff --git a/app/src/main/java/com/owncloud/android/db/OCUploadComparator.kt b/app/src/main/java/com/owncloud/android/db/OCUploadComparator.kt index 320de12..5c73c9f 100644 --- a/app/src/main/java/com/owncloud/android/db/OCUploadComparator.kt +++ b/app/src/main/java/com/owncloud/android/db/OCUploadComparator.kt @@ -45,19 +45,15 @@ class OCUploadComparator : Comparator { return 0 } - private fun compareUploadStatus(upload1: OCUpload, upload2: OCUpload): Int { - return upload1.fixedUploadStatus.compareTo(upload2.fixedUploadStatus) - } + private fun compareUploadStatus(upload1: OCUpload, upload2: OCUpload): Int = + upload1.fixedUploadStatus.compareTo(upload2.fixedUploadStatus) - private fun compareUploadingNow(upload1: OCUpload, upload2: OCUpload): Int { - return upload2.isFixedUploadingNow.compareTo(upload1.isFixedUploadingNow) - } + private fun compareUploadingNow(upload1: OCUpload, upload2: OCUpload): Int = + upload2.isFixedUploadingNow.compareTo(upload1.isFixedUploadingNow) - private fun compareUpdateTime(upload1: OCUpload, upload2: OCUpload): Int { - return upload2.fixedUploadEndTimeStamp.compareTo(upload1.fixedUploadEndTimeStamp) - } + private fun compareUpdateTime(upload1: OCUpload, upload2: OCUpload): Int = + upload2.fixedUploadEndTimeStamp.compareTo(upload1.fixedUploadEndTimeStamp) - private fun compareUploadId(upload1: OCUpload, upload2: OCUpload): Int { - return upload1.fixedUploadId.compareTo(upload2.fixedUploadId) - } + private fun compareUploadId(upload1: OCUpload, upload2: OCUpload): Int = + upload1.fixedUploadId.compareTo(upload2.fixedUploadId) } diff --git a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java index a22d5d8..46905d6 100644 --- a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java +++ b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java @@ -7,7 +7,7 @@ * SPDX-FileCopyrightText: 2014-2015 María Asensio Valverde * SPDX-FileCopyrightText: 2012 David A. Velasco * SPDX-FileCopyrightText: 2011 Bartosz Przybylski - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.db; @@ -16,8 +16,6 @@ import android.provider.BaseColumns; import com.owncloud.android.MainApp; -import java.util.Arrays; -import java.util.Collections; import java.util.List; /** @@ -25,13 +23,27 @@ import java.util.List; */ public class ProviderMeta { public static final String DB_NAME = "filelist"; - public static final int DB_VERSION = 81; + public static final int DB_VERSION = 96; private ProviderMeta() { // No instance } static public class ProviderTableMeta implements BaseColumns { + // region Recommended files table + public static final String RECOMMENDED_FILE_TABLE_NAME = "recommended_files"; + public static final String RECOMMENDED_FILE_NAME = "name"; + public static final String RECOMMENDED_FILE_ACCOUNT_NAME = "account_name"; + public static final String RECOMMENDED_FILE_DIRECTORY = "directory"; + public static final String RECOMMENDED_FILE_EXTENSIONS = "extension"; + public static final String RECOMMENDED_FILE_MIME_TYPE = "mime_type"; + public static final String RECOMMENDED_FILE_HAS_PREVIEW = "has_preview"; + public static final String RECOMMENDED_FILE_REASON = "reason"; + public static final String RECOMMENDED_TIMESTAMP = "timestamp"; + // endregion + + // region Table names + public static final String OFFLINE_OPERATION_TABLE_NAME = "offline_operations"; public static final String FILE_TABLE_NAME = "filelist"; public static final String OCSHARES_TABLE_NAME = "ocshares"; public static final String CAPABILITIES_TABLE_NAME = "capabilities"; @@ -40,31 +52,33 @@ public class ProviderMeta { public static final String EXTERNAL_LINKS_TABLE_NAME = "external_links"; public static final String ARBITRARY_DATA_TABLE_NAME = "arbitrary_data"; public static final String VIRTUAL_TABLE_NAME = "virtual"; + public static final String ASSISTANT_TABLE_NAME = "assistant"; public static final String FILESYSTEM_TABLE_NAME = "filesystem"; public static final String EDITORS_TABLE_NAME = "editors"; public static final String CREATORS_TABLE_NAME = "creators"; + // endregion private static final String CONTENT_PREFIX = "content://"; public static final Uri CONTENT_URI = Uri.parse(CONTENT_PREFIX - + MainApp.getAuthority() + "/"); + + MainApp.getAuthority() + "/"); public static final Uri CONTENT_URI_FILE = Uri.parse(CONTENT_PREFIX - + MainApp.getAuthority() + "/file"); + + MainApp.getAuthority() + "/file"); public static final Uri CONTENT_URI_DIR = Uri.parse(CONTENT_PREFIX - + MainApp.getAuthority() + "/dir"); + + MainApp.getAuthority() + "/dir"); public static final Uri CONTENT_URI_SHARE = Uri.parse(CONTENT_PREFIX - + MainApp.getAuthority() + "/shares"); + + MainApp.getAuthority() + "/shares"); public static final Uri CONTENT_URI_CAPABILITIES = Uri.parse(CONTENT_PREFIX - + MainApp.getAuthority() + "/capabilities"); + + MainApp.getAuthority() + "/capabilities"); public static final Uri CONTENT_URI_UPLOADS = Uri.parse(CONTENT_PREFIX - + MainApp.getAuthority() + "/uploads"); + + MainApp.getAuthority() + "/uploads"); public static final Uri CONTENT_URI_SYNCED_FOLDERS = Uri.parse(CONTENT_PREFIX - + MainApp.getAuthority() + "/synced_folders"); + + MainApp.getAuthority() + "/synced_folders"); public static final Uri CONTENT_URI_EXTERNAL_LINKS = Uri.parse(CONTENT_PREFIX - + MainApp.getAuthority() + "/external_links"); + + MainApp.getAuthority() + "/external_links"); public static final Uri CONTENT_URI_VIRTUAL = Uri.parse(CONTENT_PREFIX + MainApp.getAuthority() + "/virtual"); public static final Uri CONTENT_URI_FILESYSTEM = Uri.parse(CONTENT_PREFIX - + MainApp.getAuthority() + "/filesystem"); + + MainApp.getAuthority() + "/filesystem"); public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.owncloud.file"; @@ -76,6 +90,7 @@ public class ProviderMeta { public static final String FILE_ENCRYPTED_NAME = "encrypted_filename"; public static final String FILE_CREATION = "created"; public static final String FILE_MODIFIED = "modified"; + public static final String FILE_UPLOADED = "uploaded"; public static final String FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA = "modified_at_last_sync_for_data"; public static final String FILE_CONTENT_LENGTH = "content_length"; public static final String FILE_CONTENT_TYPE = "content_type"; @@ -120,58 +135,62 @@ public class ProviderMeta { public static final String FILE_LOCK_TOKEN = "lock_token"; public static final String FILE_TAGS = "tags"; public static final String FILE_E2E_COUNTER = "e2e_counter"; + public static final String FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP = "internal_two_way_sync_timestamp"; + public static final String FILE_INTERNAL_TWO_WAY_SYNC_RESULT = "internal_two_way_sync_result"; - public static final List FILE_ALL_COLUMNS = Collections.unmodifiableList(Arrays.asList( - _ID, - FILE_PARENT, - FILE_NAME, - FILE_ENCRYPTED_NAME, - FILE_CREATION, - FILE_MODIFIED, - FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA, - FILE_CONTENT_LENGTH, - FILE_CONTENT_TYPE, - FILE_STORAGE_PATH, - FILE_PATH, - FILE_PATH_DECRYPTED, - FILE_ACCOUNT_OWNER, - FILE_LAST_SYNC_DATE, - FILE_LAST_SYNC_DATE_FOR_DATA, - FILE_KEEP_IN_SYNC, - FILE_ETAG, - FILE_ETAG_ON_SERVER, - FILE_SHARED_VIA_LINK, - FILE_SHARED_WITH_SHAREE, - FILE_PERMISSIONS, - FILE_REMOTE_ID, - FILE_LOCAL_ID, - FILE_UPDATE_THUMBNAIL, - FILE_IS_DOWNLOADING, - FILE_ETAG_IN_CONFLICT, - FILE_FAVORITE, - FILE_HIDDEN, - FILE_IS_ENCRYPTED, - FILE_MOUNT_TYPE, - FILE_HAS_PREVIEW, - FILE_UNREAD_COMMENTS_COUNT, - FILE_OWNER_ID, - FILE_OWNER_DISPLAY_NAME, - FILE_NOTE, - FILE_SHAREES, - FILE_RICH_WORKSPACE, - FILE_LOCKED, - FILE_LOCK_TYPE, - FILE_LOCK_OWNER, - FILE_LOCK_OWNER_DISPLAY_NAME, - FILE_LOCK_OWNER_EDITOR, - FILE_LOCK_TIMESTAMP, - FILE_LOCK_TIMEOUT, - FILE_LOCK_TOKEN, - FILE_METADATA_SIZE, - FILE_METADATA_LIVE_PHOTO, - FILE_E2E_COUNTER, - FILE_TAGS, - FILE_METADATA_GPS)); + public static final List FILE_ALL_COLUMNS = List.of(_ID, + FILE_PARENT, + FILE_NAME, + FILE_ENCRYPTED_NAME, + FILE_UPLOADED, + FILE_CREATION, + FILE_MODIFIED, + FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA, + FILE_CONTENT_LENGTH, + FILE_CONTENT_TYPE, + FILE_STORAGE_PATH, + FILE_PATH, + FILE_PATH_DECRYPTED, + FILE_ACCOUNT_OWNER, + FILE_LAST_SYNC_DATE, + FILE_LAST_SYNC_DATE_FOR_DATA, + FILE_KEEP_IN_SYNC, + FILE_ETAG, + FILE_ETAG_ON_SERVER, + FILE_SHARED_VIA_LINK, + FILE_SHARED_WITH_SHAREE, + FILE_PERMISSIONS, + FILE_REMOTE_ID, + FILE_LOCAL_ID, + FILE_UPDATE_THUMBNAIL, + FILE_IS_DOWNLOADING, + FILE_ETAG_IN_CONFLICT, + FILE_FAVORITE, + FILE_HIDDEN, + FILE_IS_ENCRYPTED, + FILE_MOUNT_TYPE, + FILE_HAS_PREVIEW, + FILE_UNREAD_COMMENTS_COUNT, + FILE_OWNER_ID, + FILE_OWNER_DISPLAY_NAME, + FILE_NOTE, + FILE_SHAREES, + FILE_RICH_WORKSPACE, + FILE_LOCKED, + FILE_LOCK_TYPE, + FILE_LOCK_OWNER, + FILE_LOCK_OWNER_DISPLAY_NAME, + FILE_LOCK_OWNER_EDITOR, + FILE_LOCK_TIMESTAMP, + FILE_LOCK_TIMEOUT, + FILE_LOCK_TOKEN, + FILE_METADATA_SIZE, + FILE_METADATA_LIVE_PHOTO, + FILE_E2E_COUNTER, + FILE_TAGS, + FILE_METADATA_GPS, + FILE_INTERNAL_TWO_WAY_SYNC_TIMESTAMP, + FILE_INTERNAL_TWO_WAY_SYNC_RESULT); public static final String FILE_DEFAULT_SORT_ORDER = FILE_NAME + " collate nocase asc"; // Columns of ocshares table @@ -194,9 +213,12 @@ public class ProviderMeta { public static final String OCSHARES_HIDE_DOWNLOAD = "hide_download"; public static final String OCSHARES_SHARE_LINK = "share_link"; public static final String OCSHARES_SHARE_LABEL = "share_label"; + public static final String OCSHARES_DOWNLOADLIMIT_LIMIT = "download_limit_limit"; + public static final String OCSHARES_DOWNLOADLIMIT_COUNT = "download_limit_count"; + public static final String OCSHARES_ATTRIBUTES = "attributes"; public static final String OCSHARES_DEFAULT_SORT_ORDER = OCSHARES_FILE_SOURCE - + " collate nocase asc"; + + " collate nocase asc"; // Columns of capabilities table public static final String CAPABILITIES_ACCOUNT_NAME = "account"; @@ -213,11 +235,11 @@ public class ProviderMeta { public static final String CAPABILITIES_SHARING_PUBLIC_ASK_FOR_OPTIONAL_PASSWORD = "sharing_public_ask_for_optional_password"; public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENABLED = - "sharing_public_expire_date_enabled"; + "sharing_public_expire_date_enabled"; public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS = - "sharing_public_expire_date_days"; + "sharing_public_expire_date_days"; public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENFORCED = - "sharing_public_expire_date_enforced"; + "sharing_public_expire_date_enforced"; public static final String CAPABILITIES_SHARING_PUBLIC_SEND_MAIL = "sharing_public_send_mail"; public static final String CAPABILITIES_SHARING_PUBLIC_UPLOAD = "sharing_public_upload"; public static final String CAPABILITIES_SHARING_USER_SEND_MAIL = "sharing_user_send_mail"; @@ -255,10 +277,21 @@ public class ProviderMeta { public static final String CAPABILITIES_ETAG = "etag"; public static final String CAPABILITIES_USER_STATUS = "user_status"; public static final String CAPABILITIES_USER_STATUS_SUPPORTS_EMOJI = "user_status_supports_emoji"; + public static final String CAPABILITIES_USER_STATUS_SUPPORTS_BUSY = "user_status_supports_busy"; public static final String CAPABILITIES_ASSISTANT = "assistant"; public static final String CAPABILITIES_GROUPFOLDERS = "groupfolders"; public static final String CAPABILITIES_DROP_ACCOUNT = "drop_account"; public static final String CAPABILITIES_SECURITY_GUARD = "security_guard"; + public static final String CAPABILITIES_FORBIDDEN_FILENAME_CHARACTERS = "forbidden_filename_characters"; + public static final String CAPABILITIES_FORBIDDEN_FILENAMES = "forbidden_filenames"; + public static final String CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_EXTENSIONS = "forbidden_filename_extensions"; + public static final String CAPABILITIES_FORBIDDEN_FORBIDDEN_FILENAME_BASE_NAMES = "forbidden_filename_basenames"; + public static final String CAPABILITIES_WINDOWS_COMPATIBLE_FILENAMES = "windows_compatible_filenames"; + public static final String CAPABILITIES_FILES_DOWNLOAD_LIMIT = "files_download_limit"; + public static final String CAPABILITIES_FILES_DOWNLOAD_LIMIT_DEFAULT = "files_download_limit_default"; + public static final String CAPABILITIES_NOTES_FOLDER_PATH = "notes_folder_path"; + public static final String CAPABILITIES_DEFAULT_PERMISSIONS = "default_permissions"; + public static final String CAPABILITIES_HAS_VALID_SUBSCRIPTION = "has_valid_subscription"; //Columns of Uploads table public static final String UPLOADS_LOCAL_PATH = "local_path"; @@ -278,6 +311,15 @@ public class ProviderMeta { public static final String UPLOADS_IS_WIFI_ONLY = "is_wifi_only"; public static final String UPLOADS_FOLDER_UNLOCK_TOKEN = "folder_unlock_token"; + // Columns of offline operation table + public static final String OFFLINE_OPERATION_PARENT_OC_FILE_ID = "offline_operations_parent_oc_file_id"; + public static final String OFFLINE_OPERATION_TYPE = "offline_operations_type"; + public static final String OFFLINE_OPERATION_PATH = "offline_operations_path"; + public static final String OFFLINE_OPERATION_MODIFIED_AT = "offline_operations_modified_at"; + public static final String OFFLINE_OPERATION_CREATED_AT = "offline_operations_created_at"; + public static final String OFFLINE_OPERATION_FILE_NAME = "offline_operations_file_name"; + + // Columns of synced folder table public static final String SYNCED_FOLDER_LOCAL_PATH = "local_path"; public static final String SYNCED_FOLDER_REMOTE_PATH = "remote_path"; @@ -323,6 +365,8 @@ public class ProviderMeta { public static final String FILESYSTEM_SYNCED_FOLDER_ID = "syncedfolder_id"; public static final String FILESYSTEM_CRC32 = "crc32"; + public static final String CAPABILITIES_RECOMMENDATION = "recommendation"; + private ProviderTableMeta() { // No instance } diff --git a/app/src/main/java/com/owncloud/android/db/UploadResult.java b/app/src/main/java/com/owncloud/android/db/UploadResult.java index 4a38dae..8c52300 100644 --- a/app/src/main/java/com/owncloud/android/db/UploadResult.java +++ b/app/src/main/java/com/owncloud/android/db/UploadResult.java @@ -6,12 +6,14 @@ * SPDX-FileCopyrightText: 2016 ownCloud Inc. * SPDX-FileCopyrightText: 2016 David A. Velasco * SPDX-FileCopyrightText: 2015-2016 María Asensio Valverde - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.db; import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import java.util.Map; + public enum UploadResult { UNKNOWN(-1), UPLOADED(0), @@ -49,120 +51,67 @@ public enum UploadResult { return value; } + private static final Map valueMap = Map.ofEntries( + Map.entry(0, UPLOADED), + Map.entry(1, NETWORK_CONNECTION), + Map.entry(2, CREDENTIAL_ERROR), + Map.entry(3, FOLDER_ERROR), + Map.entry(4, CONFLICT_ERROR), + Map.entry(5, FILE_ERROR), + Map.entry(6, PRIVILEGES_ERROR), + Map.entry(7, CANCELLED), + Map.entry(8, FILE_NOT_FOUND), + Map.entry(9, DELAYED_FOR_WIFI), + Map.entry(10, SERVICE_INTERRUPTED), + Map.entry(11, DELAYED_FOR_CHARGING), + Map.entry(12, MAINTENANCE_MODE), + Map.entry(13, LOCK_FAILED), + Map.entry(14, DELAYED_IN_POWER_SAVE_MODE), + Map.entry(15, SSL_RECOVERABLE_PEER_UNVERIFIED), + Map.entry(16, VIRUS_DETECTED), + Map.entry(17, LOCAL_STORAGE_FULL), + Map.entry(18, OLD_ANDROID_API), + Map.entry(19, SYNC_CONFLICT), + Map.entry(20, CANNOT_CREATE_FILE), + Map.entry(21, LOCAL_STORAGE_NOT_COPIED), + Map.entry(22, QUOTA_EXCEEDED), + Map.entry(23, SAME_FILE_CONFLICT) + ); public static UploadResult fromValue(int value) { - switch (value) { - case -1: - return UNKNOWN; - case 0: - return UPLOADED; - case 1: - return NETWORK_CONNECTION; - case 2: - return CREDENTIAL_ERROR; - case 3: - return FOLDER_ERROR; - case 4: - return CONFLICT_ERROR; - case 5: - return FILE_ERROR; - case 6: - return PRIVILEGES_ERROR; - case 7: - return CANCELLED; - case 8: - return FILE_NOT_FOUND; - case 9: - return DELAYED_FOR_WIFI; - case 10: - return SERVICE_INTERRUPTED; - case 11: - return DELAYED_FOR_CHARGING; - case 12: - return MAINTENANCE_MODE; - case 13: - return LOCK_FAILED; - case 14: - return DELAYED_IN_POWER_SAVE_MODE; - case 15: - return SSL_RECOVERABLE_PEER_UNVERIFIED; - case 16: - return VIRUS_DETECTED; - case 17: - return LOCAL_STORAGE_FULL; - case 18: - return OLD_ANDROID_API; - case 19: - return SYNC_CONFLICT; - case 20: - return CANNOT_CREATE_FILE; - case 21: - return LOCAL_STORAGE_NOT_COPIED; - case 22: - return QUOTA_EXCEEDED; - case 23: - return SAME_FILE_CONFLICT; - } - return UNKNOWN; + return valueMap.getOrDefault(value, UNKNOWN); } public static UploadResult fromOperationResult(RemoteOperationResult result) { - // messy :( - switch (result.getCode()) { - case OK: - return UPLOADED; - case NO_NETWORK_CONNECTION: - case HOST_NOT_AVAILABLE: - case TIMEOUT: - case WRONG_CONNECTION: - case INCORRECT_ADDRESS: - case SSL_ERROR: - return NETWORK_CONNECTION; - case ACCOUNT_EXCEPTION: - case UNAUTHORIZED: - return CREDENTIAL_ERROR; - case FILE_NOT_FOUND: - return FOLDER_ERROR; - case LOCAL_FILE_NOT_FOUND: - return FILE_NOT_FOUND; - case CONFLICT: - return CONFLICT_ERROR; - case LOCAL_STORAGE_NOT_COPIED: - return LOCAL_STORAGE_NOT_COPIED; - case LOCAL_STORAGE_FULL: - return LOCAL_STORAGE_FULL; - case OLD_ANDROID_API: - return OLD_ANDROID_API; - case SYNC_CONFLICT: - return SYNC_CONFLICT; - case FORBIDDEN: - return PRIVILEGES_ERROR; - case CANCELLED: - return CANCELLED; - case DELAYED_FOR_WIFI: - return DELAYED_FOR_WIFI; - case DELAYED_FOR_CHARGING: - return DELAYED_FOR_CHARGING; - case DELAYED_IN_POWER_SAVE_MODE: - return DELAYED_IN_POWER_SAVE_MODE; - case MAINTENANCE_MODE: - return MAINTENANCE_MODE; - case SSL_RECOVERABLE_PEER_UNVERIFIED: - return SSL_RECOVERABLE_PEER_UNVERIFIED; - case UNKNOWN_ERROR: + return switch (result.getCode()) { + case OK -> UPLOADED; + case NO_NETWORK_CONNECTION, HOST_NOT_AVAILABLE, TIMEOUT, WRONG_CONNECTION, INCORRECT_ADDRESS, SSL_ERROR -> + NETWORK_CONNECTION; + case ACCOUNT_EXCEPTION, UNAUTHORIZED -> CREDENTIAL_ERROR; + case FILE_NOT_FOUND -> FOLDER_ERROR; + case LOCAL_FILE_NOT_FOUND -> FILE_NOT_FOUND; + case CONFLICT -> CONFLICT_ERROR; + case LOCAL_STORAGE_NOT_COPIED -> LOCAL_STORAGE_NOT_COPIED; + case LOCAL_STORAGE_FULL -> LOCAL_STORAGE_FULL; + case OLD_ANDROID_API -> OLD_ANDROID_API; + case SYNC_CONFLICT -> SYNC_CONFLICT; + case FORBIDDEN -> PRIVILEGES_ERROR; + case CANCELLED -> CANCELLED; + case DELAYED_FOR_WIFI -> DELAYED_FOR_WIFI; + case DELAYED_FOR_CHARGING -> DELAYED_FOR_CHARGING; + case DELAYED_IN_POWER_SAVE_MODE -> DELAYED_IN_POWER_SAVE_MODE; + case MAINTENANCE_MODE -> MAINTENANCE_MODE; + case SSL_RECOVERABLE_PEER_UNVERIFIED -> SSL_RECOVERABLE_PEER_UNVERIFIED; + case UNKNOWN_ERROR -> { if (result.getException() instanceof java.io.FileNotFoundException) { - return FILE_ERROR; + yield FILE_ERROR; } - return UNKNOWN; - case LOCK_FAILED: - return LOCK_FAILED; - case VIRUS_DETECTED: - return VIRUS_DETECTED; - case CANNOT_CREATE_FILE: - return CANNOT_CREATE_FILE; - case QUOTA_EXCEEDED: - return QUOTA_EXCEEDED; - default: - return UNKNOWN; - } + yield UNKNOWN; + } + case LOCK_FAILED -> LOCK_FAILED; + case VIRUS_DETECTED -> VIRUS_DETECTED; + case CANNOT_CREATE_FILE -> CANNOT_CREATE_FILE; + case QUOTA_EXCEEDED -> QUOTA_EXCEEDED; + default -> UNKNOWN; + }; } } diff --git a/app/src/main/java/com/owncloud/android/features/FeatureItem.java b/app/src/main/java/com/owncloud/android/features/FeatureItem.java index 63f4170..a389e9c 100644 --- a/app/src/main/java/com/owncloud/android/features/FeatureItem.java +++ b/app/src/main/java/com/owncloud/android/features/FeatureItem.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2018-2020 Tobias Kaminsky - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.features; diff --git a/app/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java b/app/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java index d00db54..0e367ca 100644 --- a/app/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java +++ b/app/src/main/java/com/owncloud/android/files/BootupBroadcastReceiver.java @@ -7,7 +7,7 @@ * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2014 David A. Velasco * SPDX-FileCopyrightText: 2012 Bartosz Przybylski - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.files; @@ -23,6 +23,7 @@ import com.nextcloud.client.network.ConnectivityService; import com.nextcloud.client.network.WalledCheckCache; import com.nextcloud.client.preferences.AppPreferences; import com.owncloud.android.MainApp; +import com.owncloud.android.datamodel.SyncedFolderProvider; import com.owncloud.android.datamodel.UploadsStorageManager; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.utils.theme.ViewThemeUtils; @@ -48,6 +49,7 @@ public class BootupBroadcastReceiver extends BroadcastReceiver { @Inject Clock clock; @Inject ViewThemeUtils viewThemeUtils; @Inject WalledCheckCache walledCheckCache; + @Inject SyncedFolderProvider syncedFolderProvider; /** * Receives broadcast intent reporting that the system was just boot up. * @@ -60,7 +62,8 @@ public class BootupBroadcastReceiver extends BroadcastReceiver { AndroidInjection.inject(this, context); if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { - MainApp.initSyncOperations(preferences, + MainApp.initSyncOperations(context, + preferences, uploadsStorageManager, accountManager, connectivityService, @@ -68,7 +71,9 @@ public class BootupBroadcastReceiver extends BroadcastReceiver { backgroundJobManager, clock, viewThemeUtils, - walledCheckCache); + walledCheckCache, + syncedFolderProvider + ); MainApp.initContactsBackup(accountManager, backgroundJobManager); } else { Log_OC.d(TAG, "Getting wrong intent: " + intent.getAction()); diff --git a/app/src/main/java/com/owncloud/android/files/CreateFileFromTemplateOperation.java b/app/src/main/java/com/owncloud/android/files/CreateFileFromTemplateOperation.java index 59b1449..5e146a8 100644 --- a/app/src/main/java/com/owncloud/android/files/CreateFileFromTemplateOperation.java +++ b/app/src/main/java/com/owncloud/android/files/CreateFileFromTemplateOperation.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2018 Tobias Kaminsky * SPDX-FileCopyrightText: 2018 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.files; diff --git a/app/src/main/java/com/owncloud/android/files/FetchTemplateOperation.java b/app/src/main/java/com/owncloud/android/files/FetchTemplateOperation.java index eb79c5e..e16fafa 100644 --- a/app/src/main/java/com/owncloud/android/files/FetchTemplateOperation.java +++ b/app/src/main/java/com/owncloud/android/files/FetchTemplateOperation.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2018 Tobias Kaminsky * SPDX-FileCopyrightText: 2018 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.files; diff --git a/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java b/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java index 38e855f..6665598 100644 --- a/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java +++ b/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java @@ -1,7 +1,7 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2019-2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Álvaro Brey Vilas * SPDX-FileCopyrightText: 2020 Andy Scherzinger @@ -9,7 +9,7 @@ * SPDX-FileCopyrightText: 2016 ownCloud Inc. * SPDX-FileCopyrightText: 2014-2016 David A. Velasco * SPDX-FileCopyrightText: 2012 Bartosz Przybylski - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.files; @@ -22,7 +22,9 @@ import com.nextcloud.client.account.User; import com.nextcloud.client.editimage.EditImageActivity; import com.nextcloud.client.jobs.download.FileDownloadHelper; import com.nextcloud.client.jobs.upload.FileUploadHelper; +import com.nextcloud.ui.fileactions.FileAction; import com.nextcloud.utils.EditorUtils; +import com.nextcloud.utils.mdm.MDMConfig; import com.owncloud.android.R; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; @@ -36,6 +38,7 @@ import com.owncloud.android.utils.NextcloudServer; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -169,10 +172,17 @@ public class FileMenuFilter { filterLock(toHide, fileLockingEnabled); filterUnlock(toHide, fileLockingEnabled); filterPinToHome(toHide); + filterRetry(toHide); + filterPermissionActions(toHide); return toHide; } + private void filterPermissionActions(List toHide) { + final var actionsToHide = FileAction.Companion.getActionsToHide(new HashSet<>(files)); + toHide.addAll(actionsToHide); + } + private void filterShareFile(List toHide, OCCapability capability) { if (!isSingleSelection() || containsEncryptedFile() || hasEncryptedParent() || @@ -183,9 +193,24 @@ public class FileMenuFilter { } private void filterSendFiles(List toHide, boolean inSingleFileFragment) { - if ((overflowMenu || SEND_OFF.equalsIgnoreCase(context.getString(R.string.send_files_to_other_apps)) || containsEncryptedFile()) || - (!inSingleFileFragment && (isSingleSelection() || !allFileDown())) || - !toHide.contains(R.id.action_send_share_file)) { + boolean sendFilesNotSupported = context != null && !MDMConfig.INSTANCE.sendFilesSupport(context); + boolean hasEncryptedFile = containsEncryptedFile(); + boolean isSingleSelection = isSingleSelection(); + boolean allFilesNotDown = !allFileDown(); + + if (sendFilesNotSupported) { + toHide.add(R.id.action_send_file); + return; + } + + if (overflowMenu || hasEncryptedFile) { + toHide.add(R.id.action_send_file); + return; + } + + if (!inSingleFileFragment && (isSingleSelection || allFilesNotDown)) { + toHide.add(R.id.action_send_file); + } else if (!toHide.contains(R.id.action_send_share_file)) { toHide.add(R.id.action_send_file); } } @@ -260,6 +285,12 @@ public class FileMenuFilter { } } + private void filterRetry(List toHide) { + if (!files.iterator().next().isOfflineOperation()) { + toHide.add(R.id.action_retry); + } + } + private void filterEdit( List toHide, OCCapability capability @@ -390,8 +421,10 @@ public class FileMenuFilter { } private boolean anyFileDownloading() { + final var fileDownloadHelper = FileDownloadHelper.Companion.instance(); + for (OCFile file : files) { - if (FileDownloadHelper.Companion.instance().isDownloading(user, file)) { + if (fileDownloadHelper.isDownloading(user, file)) { return true; } } @@ -401,7 +434,7 @@ public class FileMenuFilter { private boolean anyFileUploading() { for (OCFile file : files) { - if (FileUploadHelper.Companion.instance().isUploading(user, file)) { + if (FileUploadHelper.Companion.instance().isUploading(file.getRemotePath(), user.getAccountName())) { return true; } } @@ -416,13 +449,11 @@ public class FileMenuFilter { } private boolean isShareWithUsersAllowed() { - return context != null && - context.getResources().getBoolean(R.bool.share_with_users_feature); + return context != null && MDMConfig.INSTANCE.shareViaUser(context); } private boolean isShareViaLinkAllowed() { - return context != null && - context.getResources().getBoolean(R.bool.share_via_link_feature); + return context != null && MDMConfig.INSTANCE.shareViaLink(context); } private boolean isSingleSelection() { @@ -457,7 +488,7 @@ public class FileMenuFilter { } private boolean isGroupFolder() { - return files.iterator().next().isGroupFolder(); + return files.iterator().next().mounted(); } private boolean hasEncryptedParent() { diff --git a/app/src/main/java/com/owncloud/android/files/StreamMediaFileOperation.java b/app/src/main/java/com/owncloud/android/files/StreamMediaFileOperation.java index 7545fcf..6d12aed 100644 --- a/app/src/main/java/com/owncloud/android/files/StreamMediaFileOperation.java +++ b/app/src/main/java/com/owncloud/android/files/StreamMediaFileOperation.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2018 Tobias Kaminsky * SPDX-FileCopyrightText: 2018 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.files; diff --git a/app/src/main/java/com/owncloud/android/files/services/IndexedForest.java b/app/src/main/java/com/owncloud/android/files/services/IndexedForest.java index d0d3fde..b19e5eb 100644 --- a/app/src/main/java/com/owncloud/android/files/services/IndexedForest.java +++ b/app/src/main/java/com/owncloud/android/files/services/IndexedForest.java @@ -1,11 +1,11 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2017-2018 Andy Scherzinger * SPDX-FileCopyrightText: 2016 ownCloud Inc. * SPDX-FileCopyrightText: 2015 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.files.services; @@ -184,10 +184,7 @@ public class IndexedForest { } private void removeDescendants(Node removed) { - Iterator> childrenIt = removed.getChildren().iterator(); - Node child = null; - while (childrenIt.hasNext()) { - child = childrenIt.next(); + for (Node child : removed.getChildren()) { mMap.remove(child.getKey()); removeDescendants(child); } @@ -218,14 +215,7 @@ public class IndexedForest { * @param accountName */ public void remove(String accountName){ - Iterator it = mMap.keySet().iterator(); - while (it.hasNext()) { - String key = it.next(); - Log_OC.d("IndexedForest", "Number of pending downloads= " + mMap.size()); - if (key.startsWith(accountName)) { - mMap.remove(key); - } - } + mMap.keySet().removeIf(key -> key.startsWith(accountName)); } /** diff --git a/app/src/main/java/com/owncloud/android/files/services/NameCollisionPolicy.java b/app/src/main/java/com/owncloud/android/files/services/NameCollisionPolicy.java index cab2f10..071a1c0 100644 --- a/app/src/main/java/com/owncloud/android/files/services/NameCollisionPolicy.java +++ b/app/src/main/java/com/owncloud/android/files/services/NameCollisionPolicy.java @@ -2,17 +2,29 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2021 Chris Narkiewicz - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.files.services; /** - * Ordinal of enumerated constants is important for old data compatibility. + * Defines how to handle file name collisions during uploads. + * + *

Important: Enum ordinals are stored directly in the database. + * Do not change their order or remove constants to avoid breaking + * compatibility with old data.

+ * + *

Database value mapping:

+ *
    + *
  • 0 → {@link #RENAME} (old forceOverwrite = false)
  • + *
  • 1 → {@link #OVERWRITE} (old forceOverwrite = true)
  • + *
  • 2 → {@link #SKIP}
  • + *
  • 3 → {@link #ASK_USER}
  • + *
*/ public enum NameCollisionPolicy { - RENAME, // Ordinal corresponds to old forceOverwrite = false (0 in database) - OVERWRITE, // Ordinal corresponds to old forceOverwrite = true (1 in database) - CANCEL, + RENAME, + OVERWRITE, + SKIP, ASK_USER; public static final NameCollisionPolicy DEFAULT = RENAME; diff --git a/app/src/main/java/com/owncloud/android/media/MediaControlView.java b/app/src/main/java/com/owncloud/android/media/MediaControlView.java deleted file mode 100644 index 898b220..0000000 --- a/app/src/main/java/com/owncloud/android/media/MediaControlView.java +++ /dev/null @@ -1,343 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2023 Alper Ozturk - * SPDX-FileCopyrightText: 2022 Álvaro Brey Vilas - * SPDX-FileCopyrightText: 2018-2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-FileCopyrightText: 2018 Andy Scherzinger - * SPDX-FileCopyrightText: 2015 ownCloud Inc. - * SPDX-FileCopyrightText: 2013 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later - */ -package com.owncloud.android.media; - -import android.content.Context; -import android.media.MediaPlayer; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityNodeInfo; -import android.widget.LinearLayout; -import android.widget.MediaController.MediaPlayerControl; -import android.widget.SeekBar; -import android.widget.SeekBar.OnSeekBarChangeListener; - -import com.owncloud.android.MainApp; -import com.owncloud.android.R; -import com.owncloud.android.databinding.MediaControlBinding; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import java.util.Formatter; -import java.util.Locale; - -import javax.inject.Inject; - -/** - * View containing controls for a {@link MediaPlayer}. - *

- * Holds buttons "play / pause", "rewind", "fast forward" and a progress slider. - *

- * It synchronizes itself with the state of the {@link MediaPlayer}. - */ -public class MediaControlView extends LinearLayout implements OnClickListener, OnSeekBarChangeListener { - private static final String TAG = MediaControlView.class.getSimpleName(); - private static final int SHOW_PROGRESS = 1; - - private MediaPlayerControl playerControl; - private final MediaControlBinding binding; - private boolean isDragging; - - @Inject - ViewThemeUtils viewThemeUtils; - - public MediaControlView(Context context, AttributeSet attrs) { - super(context, attrs); - - MainApp.getAppComponent().inject(this); - - LayoutInflater inflate = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - binding = MediaControlBinding.inflate(inflate, this, true); - initControllerView(); - - setFocusable(true); - setFocusableInTouchMode(true); - setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); - requestFocus(); - } - - @Override - public void onFinishInflate() { - super.onFinishInflate(); - } - - public void setMediaPlayer(MediaPlayerControl player) { - playerControl = player; - handler.sendEmptyMessage(SHOW_PROGRESS); - handler.postDelayed(() -> { - updatePausePlay(); - setProgress(); - }, 100); - } - - public void stopMediaPlayerMessages() { - handler.removeMessages(SHOW_PROGRESS); - } - - private void initControllerView() { - binding.playBtn.requestFocus(); - binding.playBtn.setOnClickListener(this); - - binding.forwardBtn.setOnClickListener(this); - - binding.rewindBtn.setOnClickListener(this); - - viewThemeUtils.platform.themeHorizontalSeekBar(binding.progressBar); - binding.progressBar.setOnSeekBarChangeListener(this); - binding.progressBar.setMax(1000); - } - - /** - * Disable pause or seek buttons if the stream cannot be paused or seeked. - * This requires the control interface to be a MediaPlayerControlExt - */ - private void disableUnsupportedButtons() { - try { - if (binding != null) { - if (!playerControl.canPause()) { - binding.playBtn.setEnabled(false); - } - if (!playerControl.canSeekBackward()) { - binding.rewindBtn.setEnabled(false); - } - if (!playerControl.canSeekForward()) { - binding.forwardBtn.setEnabled(false); - } - } - - } catch (IncompatibleClassChangeError ex) { - // We were given an old version of the interface, that doesn't have - // the canPause/canSeekXYZ methods. This is OK, it just means we - // assume the media can be paused and seeked, and so we don't disable - // the buttons. - Log_OC.i(TAG, "Old media interface detected"); - } - } - - private final Handler handler = new Handler(Looper.getMainLooper()) { - @Override - public void handleMessage(Message msg) { - if (msg.what == SHOW_PROGRESS) { - updatePausePlay(); - int pos = setProgress(); - if (!isDragging) { - sendMessageDelayed(obtainMessage(SHOW_PROGRESS), 1000 - (pos % 1000)); - } - } - } - }; - - private String formatTime(int timeMs) { - int totalSeconds = timeMs / 1000; - - int seconds = totalSeconds % 60; - int minutes = (totalSeconds / 60) % 60; - int hours = totalSeconds / 3600; - - final StringBuilder mFormatBuilder = new StringBuilder(); - final Formatter mFormatter = new Formatter(mFormatBuilder, Locale.getDefault()); - if (hours > 0) { - return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString(); - } else { - return mFormatter.format("%02d:%02d", minutes, seconds).toString(); - } - } - - private int setProgress() { - if (playerControl == null || isDragging) { - return 0; - } - int position = playerControl.getCurrentPosition(); - int duration = playerControl.getDuration(); - if (binding != null) { - if (duration > 0) { - // use long to avoid overflow - long pos = 1000L * position / duration; - binding.progressBar.setProgress((int) pos); - } - int percent = playerControl.getBufferPercentage(); - binding.progressBar.setSecondaryProgress(percent * 10); - - String endTime = duration > 0 ? formatTime(duration) : "--:--"; - binding.totalTimeText.setText(endTime); - binding.currentTimeText.setText(formatTime(position)); - } - - return position; - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - int keyCode = event.getKeyCode(); - final boolean uniqueDown = event.getRepeatCount() == 0 - && event.getAction() == KeyEvent.ACTION_DOWN; - if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK - || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE - || keyCode == KeyEvent.KEYCODE_SPACE) { - if (uniqueDown) { - doPauseResume(); - //show(sDefaultTimeout); - if (binding != null) { - binding.playBtn.requestFocus(); - } - } - return true; - } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { - if (uniqueDown && !playerControl.isPlaying()) { - playerControl.start(); - updatePausePlay(); - } - return true; - } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP - || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { - if (uniqueDown && playerControl.isPlaying()) { - playerControl.pause(); - updatePausePlay(); - } - return true; - } - - return super.dispatchKeyEvent(event); - } - - public void updatePausePlay() { - if (binding == null) { - return; - } - - if (playerControl.isPlaying()) { - binding.playBtn.setImageResource(android.R.drawable.ic_media_pause); - } else { - binding.playBtn.setImageResource(android.R.drawable.ic_media_play); - } - - final boolean canSeekFfd = playerControl.canSeekForward(); - if (canSeekFfd) { - binding.forwardBtn.setVisibility(View.VISIBLE); - } else { - binding.forwardBtn.setVisibility(View.INVISIBLE); - } - - final boolean canSeekBwd = playerControl.canSeekBackward(); - if (canSeekBwd) { - binding.rewindBtn.setVisibility(View.VISIBLE); - } else { - binding.rewindBtn.setVisibility(View.INVISIBLE); - } - } - - private void doPauseResume() { - if (playerControl.isPlaying()) { - playerControl.pause(); - } else { - playerControl.start(); - } - updatePausePlay(); - } - - @Override - public void setEnabled(boolean enabled) { - if(binding!=null){ - binding.playBtn.setEnabled(enabled); - binding.forwardBtn.setEnabled(enabled); - binding.rewindBtn.setEnabled(enabled); - binding.progressBar.setEnabled(enabled); - } - - disableUnsupportedButtons(); - super.setEnabled(enabled); - } - - @Override - public void onClick(View v) { - int pos; - boolean playing = playerControl.isPlaying(); - int id = v.getId(); - - if (id == R.id.playBtn) { - doPauseResume(); - } else if (id == R.id.rewindBtn) { - pos = playerControl.getCurrentPosition(); - pos -= 5000; - playerControl.seekTo(pos); - if (!playing) { - playerControl.pause(); // necessary in some 2.3.x devices - } - setProgress(); - } else if (id == R.id.forwardBtn) { - pos = playerControl.getCurrentPosition(); - pos += 15000; - playerControl.seekTo(pos); - if (!playing) { - playerControl.pause(); // necessary in some 2.3.x devices - } - setProgress(); - } - } - - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (!fromUser) { - // We're not interested in programmatically generated changes to - // the progress bar's position. - return; - } - - long duration = playerControl.getDuration(); - long newPosition = (duration * progress) / 1000L; - playerControl.seekTo((int) newPosition); - binding.currentTimeText.setText(formatTime((int) newPosition)); - } - - /** - * Called in devices with touchpad when the user starts to adjust the position of the seekbar's thumb. - * - * Will be followed by several onProgressChanged notifications. - */ - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - isDragging = true; // monitors the duration of dragging - handler.removeMessages(SHOW_PROGRESS); // grants no more updates with media player progress while dragging - } - - /** - * Called in devices with touchpad when the user finishes the adjusting of the seekbar. - */ - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - isDragging = false; - setProgress(); - updatePausePlay(); - handler.sendEmptyMessage(SHOW_PROGRESS); // grants future updates with media player progress - } - - @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(MediaControlView.class.getName()); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(MediaControlView.class.getName()); - } -} diff --git a/app/src/main/java/com/owncloud/android/media/MediaControlView.kt b/app/src/main/java/com/owncloud/android/media/MediaControlView.kt new file mode 100644 index 0000000..217c1c1 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/media/MediaControlView.kt @@ -0,0 +1,355 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2022 Álvaro Brey Vilas + * SPDX-FileCopyrightText: 2018-2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2019 Chris Narkiewicz + * SPDX-FileCopyrightText: 2018 Andy Scherzinger + * SPDX-FileCopyrightText: 2015 ownCloud Inc. + * SPDX-FileCopyrightText: 2013 David A. Velasco + * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + */ +package com.owncloud.android.media + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.os.Message +import android.util.AttributeSet +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo +import android.widget.LinearLayout +import android.widget.SeekBar +import android.widget.SeekBar.OnSeekBarChangeListener +import androidx.core.content.ContextCompat +import androidx.media3.common.Player +import com.owncloud.android.MainApp +import com.owncloud.android.R +import com.owncloud.android.databinding.MediaControlBinding +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.utils.theme.ViewThemeUtils +import java.util.Formatter +import java.util.Locale +import javax.inject.Inject + +/** + * View containing controls for a MediaPlayer. + * + * + * Holds buttons "play / pause", "rewind", "fast forward" and a progress slider. + * + * + * It synchronizes itself with the state of the MediaPlayer. + */ +class MediaControlView(context: Context, attrs: AttributeSet?) : + LinearLayout(context, attrs), + View.OnClickListener, + OnSeekBarChangeListener { + + private var playerControl: Player? = null + private var binding: MediaControlBinding + private var isDragging = false + + @Inject + lateinit var viewThemeUtils: ViewThemeUtils + + public override fun onFinishInflate() { + super.onFinishInflate() + } + + @Suppress("MagicNumber") + fun setMediaPlayer(player: Player?) { + playerControl = player + handler.sendEmptyMessage(SHOW_PROGRESS) + + handler.postDelayed({ + updatePausePlay() + setProgress() + }, 100) + } + + @Suppress("MagicNumber") + private fun initControllerView() { + binding.playBtn.requestFocus() + + binding.playBtn.setOnClickListener(this) + binding.forwardBtn.setOnClickListener(this) + binding.rewindBtn.setOnClickListener(this) + + binding.progressBar.run { + viewThemeUtils.platform.themeHorizontalSeekBar(this) + setMax(1000) + } + + binding.progressBar.setOnSeekBarChangeListener(this) + + viewThemeUtils.material.run { + colorMaterialButtonPrimaryTonal(binding.rewindBtn) + colorMaterialButtonPrimaryTonal(binding.playBtn) + colorMaterialButtonPrimaryTonal(binding.forwardBtn) + } + } + + /** + * Disable pause or seek buttons if the stream cannot be paused or seeked. + * This requires the control interface to be a MediaPlayerControlExt + */ + private fun disableUnsupportedButtons() { + try { + if (playerControl?.isCommandAvailable(Player.COMMAND_PLAY_PAUSE)?.not() == true) { + binding.playBtn.isEnabled = false + } + + if (playerControl?.isCommandAvailable(Player.COMMAND_SEEK_BACK)?.not() == true) { + binding.rewindBtn.isEnabled = false + } + if (playerControl?.isCommandAvailable(Player.COMMAND_SEEK_FORWARD)?.not() == true) { + binding.forwardBtn.isEnabled = false + } + } catch (ex: IncompatibleClassChangeError) { + // We were given an old version of the interface, that doesn't have + // the canPause/canSeekXYZ methods. This is OK, it just means we + // assume the media can be paused and seeked, and so we don't disable + // the buttons. + Log_OC.i(TAG, "Old media interface detected") + } + } + + @Suppress("MagicNumber") + private val handler: Handler = object : Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + if (msg.what == SHOW_PROGRESS) { + updatePausePlay() + val pos = setProgress() + + if (!isDragging) { + sendMessageDelayed(obtainMessage(SHOW_PROGRESS), (1000 - pos % 1000)) + } + } + } + } + + init { + MainApp.getAppComponent().inject(this) + + val inflate = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + binding = MediaControlBinding.inflate(inflate, this, true) + initControllerView() + isFocusable = true + setFocusableInTouchMode(true) + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS) + requestFocus() + } + + @Suppress("MagicNumber") + private fun formatTime(timeMs: Long): String { + val totalSeconds = timeMs / 1000 + val seconds = totalSeconds % 60 + val minutes = totalSeconds / 60 % 60 + val hours = totalSeconds / 3600 + val mFormatBuilder = StringBuilder() + val mFormatter = Formatter(mFormatBuilder, Locale.getDefault()) + return if (hours > 0) { + mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString() + } else { + mFormatter.format("%02d:%02d", minutes, seconds).toString() + } + } + + @Suppress("MagicNumber") + private fun setProgress(): Long { + var position = 0L + if (playerControl == null || isDragging) { + position = 0 + } + + playerControl?.let { playerControl -> + position = playerControl.currentPosition + val duration = playerControl.duration + if (duration > 0) { + // use long to avoid overflow + val pos = 1000L * position / duration + binding.progressBar.progress = pos.toInt() + } + val percent = playerControl.bufferedPercentage + binding.progressBar.setSecondaryProgress(percent * 10) + val endTime = if (duration > 0) formatTime(duration) else "--:--" + binding.totalTimeText.text = endTime + binding.currentTimeText.text = formatTime(position) + } + + return position + } + + @Suppress("ReturnCount") + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + val keyCode = event.keyCode + val uniqueDown = (event.repeatCount == 0 && event.action == KeyEvent.ACTION_DOWN) + + when (keyCode) { + KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_SPACE -> { + if (uniqueDown) { + doPauseResume() + // show(sDefaultTimeout); + binding.playBtn.requestFocus() + } + return true + } + + KeyEvent.KEYCODE_MEDIA_PLAY -> { + if (uniqueDown && playerControl?.playWhenReady == false) { + playerControl?.play() + updatePausePlay() + } + return true + } + + KeyEvent.KEYCODE_MEDIA_STOP, + KeyEvent.KEYCODE_MEDIA_PAUSE + -> { + if (uniqueDown && playerControl?.playWhenReady == true) { + playerControl?.pause() + updatePausePlay() + } + return true + } + + else -> return super.dispatchKeyEvent(event) + } + } + + fun updatePausePlay() { + binding.playBtn.icon = ContextCompat.getDrawable( + context, + // use isPlaying instead of playWhenReady + // it represents only the play/pause state + // which is needed to show play/pause icons + if (playerControl?.isPlaying == true) { + R.drawable.ic_pause + } else { + R.drawable.ic_play + } + ) + binding.forwardBtn.visibility = if (playerControl?.isCommandAvailable(Player.COMMAND_SEEK_FORWARD) == true) { + VISIBLE + } else { + INVISIBLE + } + binding.rewindBtn.visibility = if (playerControl?.isCommandAvailable(Player.COMMAND_SEEK_BACK) == true) { + VISIBLE + } else { + INVISIBLE + } + } + + private fun doPauseResume() { + playerControl?.run { + if (playWhenReady) { + pause() + } else { + play() + } + } + updatePausePlay() + } + + override fun setEnabled(enabled: Boolean) { + binding.playBtn.setEnabled(enabled) + binding.forwardBtn.setEnabled(enabled) + binding.rewindBtn.setEnabled(enabled) + binding.progressBar.setEnabled(enabled) + + disableUnsupportedButtons() + + super.setEnabled(enabled) + } + + @Suppress("MagicNumber") + override fun onClick(v: View) { + playerControl?.let { playerControl -> + val playing = playerControl.playWhenReady + val id = v.id + + when (id) { + R.id.playBtn -> { + doPauseResume() + } + + R.id.rewindBtn -> { + playerControl.seekBack() + if (!playing) { + playerControl.pause() // necessary in some 2.3.x devices + } + setProgress() + } + + R.id.forwardBtn -> { + playerControl.seekForward() + if (!playing) { + playerControl.pause() // necessary in some 2.3.x devices + } + + setProgress() + } + + else -> { + } + } + } + } + + @Suppress("MagicNumber") + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + if (!fromUser) { + // We're not interested in programmatically generated changes to + // the progress bar's position. + return + } + + playerControl?.let { playerControl -> + val duration = playerControl.duration + val newPosition = duration * progress / 1000L + playerControl.seekTo(newPosition) + binding.currentTimeText.text = formatTime(newPosition) + } + } + + /** + * Called in devices with touchpad when the user starts to adjust the position of the seekbar's thumb. + * + * Will be followed by several onProgressChanged notifications. + */ + override fun onStartTrackingTouch(seekBar: SeekBar) { + isDragging = true // monitors the duration of dragging + handler.removeMessages(SHOW_PROGRESS) // grants no more updates with media player progress while dragging + } + + /** + * Called in devices with touchpad when the user finishes the adjusting of the seekbar. + */ + override fun onStopTrackingTouch(seekBar: SeekBar) { + isDragging = false + setProgress() + updatePausePlay() + handler.sendEmptyMessage(SHOW_PROGRESS) // grants future updates with media player progress + } + + override fun onInitializeAccessibilityEvent(event: AccessibilityEvent) { + super.onInitializeAccessibilityEvent(event) + event.setClassName(MediaControlView::class.java.getName()) + } + + override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) { + super.onInitializeAccessibilityNodeInfo(info) + info.setClassName(MediaControlView::class.java.getName()) + } + + companion object { + private val TAG = MediaControlView::class.java.getSimpleName() + private const val SHOW_PROGRESS = 1 + } +} diff --git a/app/src/main/java/com/owncloud/android/operations/CheckCurrentCredentialsOperation.java b/app/src/main/java/com/owncloud/android/operations/CheckCurrentCredentialsOperation.java index c8a69e3..7304ed2 100644 --- a/app/src/main/java/com/owncloud/android/operations/CheckCurrentCredentialsOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CheckCurrentCredentialsOperation.java @@ -6,7 +6,7 @@ * SPDX-FileCopyrightText: 2018 Andy Scherzinger * SPDX-FileCopyrightText: 2016 ownCloud Inc. * SPDX-FileCopyrightText: 2016 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations; diff --git a/app/src/main/java/com/owncloud/android/operations/CommentFileOperation.java b/app/src/main/java/com/owncloud/android/operations/CommentFileOperation.java index fdf16a8..5984d17 100644 --- a/app/src/main/java/com/owncloud/android/operations/CommentFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CommentFileOperation.java @@ -3,11 +3,11 @@ * * SPDX-FileCopyrightText: 2018 Tobias Kaminsky * SPDX-FileCopyrightText: 2018 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.operations; -import com.owncloud.android.lib.common.OwnCloudClient; +import com.nextcloud.common.NextcloudClient; import com.owncloud.android.lib.common.operations.RemoteOperation; import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.common.utils.Log_OC; @@ -16,7 +16,7 @@ import com.owncloud.android.lib.resources.comments.CommentFileRemoteOperation; /** * Comment file */ -public class CommentFileOperation extends RemoteOperation { +public class CommentFileOperation extends RemoteOperation { private final String message; private final long fileId; @@ -37,8 +37,8 @@ public class CommentFileOperation extends RemoteOperation { * @param client Client object to communicate with the remote ownCloud server. */ @Override - protected RemoteOperationResult run(OwnCloudClient client) { - RemoteOperationResult result = new CommentFileRemoteOperation(message, fileId).execute(client); + public RemoteOperationResult run(NextcloudClient client) { + RemoteOperationResult result = new CommentFileRemoteOperation(message, fileId).execute(client); if (!result.isSuccess()) { Log_OC.e(this, "File with Id " + fileId + " could not be commented"); diff --git a/app/src/main/java/com/owncloud/android/operations/CopyFileOperation.java b/app/src/main/java/com/owncloud/android/operations/CopyFileOperation.java index eba4565..bd7d21a 100644 --- a/app/src/main/java/com/owncloud/android/operations/CopyFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CopyFileOperation.java @@ -5,7 +5,7 @@ * SPDX-FileCopyrightText: 2019 Andy Scherzinger * SPDX-FileCopyrightText: 2012-2014 ownCloud Inc. * SPDX-FileCopyrightText: 2014 Jorge Antonio Diaz-Benito Soriano - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations; @@ -64,6 +64,19 @@ public class CopyFileOperation extends SyncOperation { if (file.isFolder()) { targetPath += OCFile.PATH_SEPARATOR; } + + // auto rename, to allow copy + if (targetPath.equals(srcPath)) { + if (file.isFolder()) { + targetPath = targetParentPath + file.getFileName(); + } + targetPath = UploadFileOperation.getNewAvailableRemotePath(client, targetPath, null, false); + + if (file.isFolder()) { + targetPath += OCFile.PATH_SEPARATOR; + } + } + RemoteOperationResult result = new CopyFileRemoteOperation(srcPath, targetPath, false).execute(client); /// 3. local copy diff --git a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java index ad16496..c2d6e39 100644 --- a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java @@ -7,7 +7,7 @@ * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2012 David A. Velasco * SPDX-FileCopyrightText: 2015 María Asensio Valverde - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations; @@ -44,6 +44,7 @@ import java.io.File; import java.util.UUID; import androidx.annotation.NonNull; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR; import static com.owncloud.android.datamodel.OCFile.ROOT_PATH; @@ -109,6 +110,10 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper } } + @SuppressFBWarnings( + value = "EXS_EXCEPTION_SOFTENING_NO_CONSTRAINTS", + justification = "Converting checked exception to runtime is acceptable in this context" + ) private RemoteOperationResult encryptedCreateV1(OCFile parent, OwnCloudClient client) { ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(context); String privateKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PRIVATE_KEY); @@ -186,20 +191,24 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper } } - RemoteOperationResult remoteFolderOperationResult = new ReadFolderRemoteOperation(encryptedRemotePath) + final var remoteFolderOperationResult = new ReadFolderRemoteOperation(encryptedRemotePath) .execute(client); - createdRemoteFolder = (RemoteFile) remoteFolderOperationResult.getData().get(0); - OCFile newDir = createRemoteFolderOcFile(parent, filename, createdRemoteFolder); - getStorageManager().saveFile(newDir); + if (remoteFolderOperationResult.isSuccess() && remoteFolderOperationResult.getData().get(0) instanceof RemoteFile remoteFile) { + createdRemoteFolder = remoteFile; + OCFile newDir = createRemoteFolderOcFile(parent, filename, createdRemoteFolder); + getStorageManager().saveFile(newDir); - RemoteOperationResult encryptionOperationResult = new ToggleEncryptionRemoteOperation( - newDir.getLocalId(), - newDir.getRemotePath(), - true) - .execute(client); + final var encryptionOperationResult = new ToggleEncryptionRemoteOperation( + newDir.getLocalId(), + newDir.getRemotePath(), + true) + .execute(client); - if (!encryptionOperationResult.isSuccess()) { + if (!encryptionOperationResult.isSuccess()) { + throw new RuntimeException("Error creating encrypted subfolder!"); + } + } else { throw new RuntimeException("Error creating encrypted subfolder!"); } } else { @@ -243,6 +252,10 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper } } + @SuppressFBWarnings( + value = "EXS_EXCEPTION_SOFTENING_NO_CONSTRAINTS", + justification = "Converting checked exception to runtime is acceptable in this context" + ) private RemoteOperationResult encryptedCreateV2(OCFile parent, OwnCloudClient client) { String token = null; Boolean metadataExists; @@ -324,20 +337,24 @@ public class CreateFolderOperation extends SyncOperation implements OnRemoteOper throw new RuntimeException("Could not unlock folder!"); } - RemoteOperationResult remoteFolderOperationResult = new ReadFolderRemoteOperation(encryptedRemotePath) + final var remoteFolderOperationResult = new ReadFolderRemoteOperation(encryptedRemotePath) .execute(client); - createdRemoteFolder = (RemoteFile) remoteFolderOperationResult.getData().get(0); - OCFile newDir = createRemoteFolderOcFile(parent, filename, createdRemoteFolder); - getStorageManager().saveFile(newDir); + if (remoteFolderOperationResult.isSuccess() && remoteFolderOperationResult.getData().get(0) instanceof RemoteFile remoteFile) { + createdRemoteFolder = remoteFile; + OCFile newDir = createRemoteFolderOcFile(parent, filename, createdRemoteFolder); + getStorageManager().saveFile(newDir); - RemoteOperationResult encryptionOperationResult = new ToggleEncryptionRemoteOperation( - newDir.getLocalId(), - newDir.getRemotePath(), - true) - .execute(client); + final var encryptionOperationResult = new ToggleEncryptionRemoteOperation( + newDir.getLocalId(), + newDir.getRemotePath(), + true) + .execute(client); - if (!encryptionOperationResult.isSuccess()) { + if (!encryptionOperationResult.isSuccess()) { + throw new RuntimeException("Error creating encrypted subfolder!"); + } + } else { throw new RuntimeException("Error creating encrypted subfolder!"); } } else { diff --git a/app/src/main/java/com/owncloud/android/operations/CreateShareViaLinkOperation.java b/app/src/main/java/com/owncloud/android/operations/CreateShareViaLinkOperation.java index 2c9377c..9dcbaa1 100644 --- a/app/src/main/java/com/owncloud/android/operations/CreateShareViaLinkOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CreateShareViaLinkOperation.java @@ -6,7 +6,7 @@ * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2016 David A. Velasco * SPDX-FileCopyrightText: 2014 María Asensio Valverde - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations; @@ -75,11 +75,7 @@ public class CreateShareViaLinkOperation extends SyncOperation { private void updateData(OCShare share) { // Update DB with the response share.setPath(path); - if (path.endsWith(FileUtils.PATH_SEPARATOR)) { - share.setFolder(true); - } else { - share.setFolder(false); - } + share.setFolder(path.endsWith(FileUtils.PATH_SEPARATOR)); getStorageManager().saveShare(share); diff --git a/app/src/main/java/com/owncloud/android/operations/CreateShareWithShareeOperation.java b/app/src/main/java/com/owncloud/android/operations/CreateShareWithShareeOperation.java index bcae519..28e7320 100644 --- a/app/src/main/java/com/owncloud/android/operations/CreateShareWithShareeOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CreateShareWithShareeOperation.java @@ -6,7 +6,7 @@ * SPDX-FileCopyrightText: 2016 Andy Scherzinger * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2015 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations; @@ -17,6 +17,7 @@ import com.nextcloud.client.account.User; import com.nextcloud.client.network.ClientFactory; import com.nextcloud.client.network.ClientFactoryImpl; import com.nextcloud.common.NextcloudClient; +import com.nextcloud.utils.extensions.DecryptedUserExtensionsKt; import com.owncloud.android.R; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.FileDataStorageManager; @@ -55,12 +56,14 @@ public class CreateShareWithShareeOperation extends SyncOperation { private String label; private final Context context; private final User user; + private String attributes; private ArbitraryDataProvider arbitraryDataProvider; private static final Set supportedShareTypes = new HashSet<>(Arrays.asList(ShareType.USER, ShareType.GROUP, ShareType.FEDERATED, + ShareType.FEDERATED_GROUP, ShareType.EMAIL, ShareType.ROOM, ShareType.CIRCLE)); @@ -84,6 +87,7 @@ public class CreateShareWithShareeOperation extends SyncOperation { String sharePassword, long expirationDateInMillis, boolean hideFileDownload, + String attributes, FileDataStorageManager storageManager, Context context, User user, @@ -104,6 +108,7 @@ public class CreateShareWithShareeOperation extends SyncOperation { this.context = context; this.user = user; this.arbitraryDataProvider = arbitraryDataProvider; + this.attributes = attributes; } @Override @@ -123,7 +128,7 @@ public class CreateShareWithShareeOperation extends SyncOperation { try { String publicKey = EncryptionUtils.getPublicKey(user, shareeName, arbitraryDataProvider); - if (publicKey.equals("")) { + if (publicKey.isEmpty()) { NextcloudClient nextcloudClient = new ClientFactoryImpl(context).createNextcloudClient(user); RemoteOperationResult result = new GetPublicKeyRemoteOperation(shareeName).execute(nextcloudClient); if (result.isSuccess()) { @@ -155,7 +160,8 @@ public class CreateShareWithShareeOperation extends SyncOperation { false, sharePassword, permissions, - noteMessage + noteMessage, + attributes ); operation.setGetShareDetails(true); RemoteOperationResult shareResult = operation.execute(client); @@ -183,7 +189,7 @@ public class CreateShareWithShareeOperation extends SyncOperation { if (metadata == null) { String cert = EncryptionUtils.retrievePublicKeyForUser(user, context); metadata = new EncryptionUtilsV2().createDecryptedFolderMetadataFile(); - metadata.getUsers().add(new DecryptedUser(client.getUserId(), cert)); + metadata.getUsers().add(new DecryptedUser(client.getUserId(), cert, null)); metadataExists = false; } else { @@ -194,9 +200,12 @@ public class CreateShareWithShareeOperation extends SyncOperation { // add sharee to metadata String publicKey = EncryptionUtils.getPublicKey(user, shareeName, arbitraryDataProvider); + + String decryptedMetadataKey = DecryptedUserExtensionsKt.findMetadataKeyByUserId(metadata.getUsers(), shareeName); DecryptedFolderMetadataFile newMetadata = encryptionUtilsV2.addShareeToMetadata(metadata, shareeName, - publicKey); + publicKey, + decryptedMetadataKey); // upload metadata metadata.getMetadata().setCounter(newCounter); diff --git a/app/src/main/java/com/owncloud/android/operations/DetectAuthenticationMethodOperation.java b/app/src/main/java/com/owncloud/android/operations/DetectAuthenticationMethodOperation.java index f5812d5..c1a5f7e 100644 --- a/app/src/main/java/com/owncloud/android/operations/DetectAuthenticationMethodOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/DetectAuthenticationMethodOperation.java @@ -5,7 +5,7 @@ * SPDX-FileCopyrightText: 2016-2017 Andy Scherzinger * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2014 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations; @@ -70,7 +70,6 @@ public class DetectAuthenticationMethodOperation extends RemoteOperation { */ @Override protected RemoteOperationResult run(OwnCloudClient client) { - RemoteOperationResult result = null; AuthenticationMethod authMethod = AuthenticationMethod.UNKNOWN; RemoteOperation operation = new ExistenceCheckRemoteOperation("", mContext, false); @@ -78,7 +77,7 @@ public class DetectAuthenticationMethodOperation extends RemoteOperation { client.setFollowRedirects(false); // try to access the root folder, following redirections but not SAML SSO redirections - result = operation.execute(client); + RemoteOperationResult result = operation.execute(client); String redirectedLocation = result.getRedirectedLocation(); while (!TextUtils.isEmpty(redirectedLocation) && !result.isIdPRedirection()) { client.setBaseUri(Uri.parse(result.getRedirectedLocation())); @@ -119,18 +118,13 @@ public class DetectAuthenticationMethodOperation extends RemoteOperation { } private String authenticationMethodToString(AuthenticationMethod value) { - switch (value) { - case NONE: - return "NONE"; - case BASIC_HTTP_AUTH: - return "BASIC_HTTP_AUTH"; - case BEARER_TOKEN: - return "BEARER_TOKEN"; - case SAML_WEB_SSO: - return "SAML_WEB_SSO"; - default: - return "UNKNOWN"; - } + return switch (value) { + case NONE -> "NONE"; + case BASIC_HTTP_AUTH -> "BASIC_HTTP_AUTH"; + case BEARER_TOKEN -> "BEARER_TOKEN"; + case SAML_WEB_SSO -> "SAML_WEB_SSO"; + default -> "UNKNOWN"; + }; } } diff --git a/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java b/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java index ec70fd4..d7b50d7 100644 --- a/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java @@ -1,21 +1,25 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2020-2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2019 Andy Scherzinger * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2015 María Asensio Valverde * SPDX-FileCopyrightText: 2012 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations; import android.content.Context; +import android.os.Handler; +import android.os.Looper; import android.text.TextUtils; import android.webkit.MimeTypeMap; import com.nextcloud.client.account.User; +import com.nextcloud.utils.extensions.ContextExtensionsKt; +import com.owncloud.android.R; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; @@ -34,8 +38,9 @@ import com.owncloud.android.utils.FileExportUtils; import com.owncloud.android.utils.FileStorageUtils; import java.io.File; -import java.io.FileOutputStream; +import java.io.IOException; import java.lang.ref.WeakReference; +import java.nio.file.Files; import java.util.HashSet; import java.util.Iterator; import java.util.Set; @@ -60,11 +65,14 @@ public class DownloadFileOperation extends RemoteOperation { private DownloadType downloadType; private final WeakReference context; + + // CHECK: Is this still needed after conversion from Foreground Services to Worker? private Set dataTransferListeners = new HashSet<>(); + private long modificationTimestamp; private DownloadFileRemoteOperation downloadOperation; - private final AtomicBoolean cancellationRequested = new AtomicBoolean(false); + private final Handler mainThreadHandler = new Handler(Looper.getMainLooper()); public DownloadFileOperation(User user, OCFile file, @@ -109,7 +117,11 @@ public class DownloadFileOperation extends RemoteOperation { if (file.getStoragePath() != null) { File parentFile = new File(file.getStoragePath()).getParentFile(); if (parentFile != null && !parentFile.exists()) { - parentFile.mkdirs(); + try { + Files.createDirectories(parentFile.toPath()); + } catch (IOException e) { + return FileStorageUtils.getDefaultSavePathFor(user.getAccountName(), file); + } } File path = new File(file.getStoragePath()); // re-downloads should be done over the original file if (path.canWrite() || parentFile != null && parentFile.canWrite()) { @@ -163,13 +175,19 @@ public class DownloadFileOperation extends RemoteOperation { /// perform the download synchronized(cancellationRequested) { if (cancellationRequested.get()) { - return new RemoteOperationResult(new OperationCancelledException()); + return new RemoteOperationResult<>(new OperationCancelledException()); } } + final var isValidExtFilename = FileStorageUtils.isValidExtFilename(file.getFileName()); + if (!isValidExtFilename) { + mainThreadHandler.post(() -> ContextExtensionsKt.showToast(context.get(), R.string.download_download_invalid_local_file_name)); + return new RemoteOperationResult<>(RemoteOperationResult.ResultCode.INVALID_CHARACTER_IN_NAME); + } + Context operationContext = context.get(); if (operationContext == null) { - return new RemoteOperationResult(RemoteOperationResult.ResultCode.UNKNOWN_ERROR); + return new RemoteOperationResult<>(RemoteOperationResult.ResultCode.UNKNOWN_ERROR); } RemoteOperationResult result; @@ -184,10 +202,7 @@ public class DownloadFileOperation extends RemoteOperation { downloadOperation = new DownloadFileRemoteOperation(file.getRemotePath(), tmpFolder); if (downloadType == DownloadType.DOWNLOAD) { - Iterator listener = dataTransferListeners.iterator(); - while (listener.hasNext()) { - downloadOperation.addDatatransferProgressListener(listener.next()); - } + dataTransferListeners.forEach(downloadOperation::addDatatransferProgressListener); } result = downloadOperation.execute(client); diff --git a/app/src/main/java/com/owncloud/android/operations/DownloadType.kt b/app/src/main/java/com/owncloud/android/operations/DownloadType.kt index 812e673..fafa110 100644 --- a/app/src/main/java/com/owncloud/android/operations/DownloadType.kt +++ b/app/src/main/java/com/owncloud/android/operations/DownloadType.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2022 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.operations @@ -11,7 +11,5 @@ enum class DownloadType(var type: String) { DOWNLOAD("DOWNLOAD"), EXPORT("EXPORT"); - override fun toString(): String { - return type - } + override fun toString(): String = type } diff --git a/app/src/main/java/com/owncloud/android/operations/GetCapabilitiesOperation.java b/app/src/main/java/com/owncloud/android/operations/GetCapabilitiesOperation.java index b151a49..9e86d16 100644 --- a/app/src/main/java/com/owncloud/android/operations/GetCapabilitiesOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/GetCapabilitiesOperation.java @@ -6,7 +6,7 @@ * SPDX-FileCopyrightText: 2020 Daniel Kesselberg * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2015 María Asensio Valverde - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations; diff --git a/app/src/main/java/com/owncloud/android/operations/GetFilesDownloadLimitOperation.kt b/app/src/main/java/com/owncloud/android/operations/GetFilesDownloadLimitOperation.kt new file mode 100644 index 0000000..b3b35c5 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/GetFilesDownloadLimitOperation.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 ZetaTom <70907959+zetatom@users.noreply.github.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.operations + +import com.nextcloud.android.lib.resources.files.FileDownloadLimit +import com.nextcloud.android.lib.resources.files.GetFilesDownloadLimitRemoteOperation +import com.nextcloud.common.NextcloudClient +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.resources.shares.OCShare +import com.owncloud.android.operations.common.SyncOperation + +class GetFilesDownloadLimitOperation(val share: OCShare, storageManager: FileDataStorageManager) : + SyncOperation( + storageManager + ) { + override fun run(client: NextcloudClient): RemoteOperationResult> { + val token = share.token ?: return RemoteOperationResult(RemoteOperationResult.ResultCode.SHARE_NOT_FOUND) + val operation = GetFilesDownloadLimitRemoteOperation(token) + + val result = operation.execute(client) + + return result + } +} diff --git a/app/src/main/java/com/owncloud/android/operations/GetServerInfoOperation.java b/app/src/main/java/com/owncloud/android/operations/GetServerInfoOperation.java index 2f6cf72..5f402d0 100644 --- a/app/src/main/java/com/owncloud/android/operations/GetServerInfoOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/GetServerInfoOperation.java @@ -6,7 +6,7 @@ * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2015 María Asensio Valverde * SPDX-FileCopyrightText: 2014 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations; @@ -46,7 +46,7 @@ public class GetServerInfoOperation extends RemoteOperation { * TODO ugly dependency, get rid of it. */ public GetServerInfoOperation(String url, Context context) { - mUrl = trimWebdavSuffix(url); + mUrl = AuthenticatorUrlUtils.INSTANCE.trimWebdavSuffix(url); mContext = context; mResultData = new ServerInfo(); } @@ -77,7 +77,7 @@ public class GetServerInfoOperation extends RemoteOperation { // third: merge results if (detectAuthResult.isSuccess()) { mResultData.mAuthMethod = (AuthenticationMethod) detectAuthResult.getData().get(0); - ArrayList data = new ArrayList(); + ArrayList data = new ArrayList<>(); data.add(mResultData); result.setData(data); } else { @@ -95,24 +95,6 @@ public class GetServerInfoOperation extends RemoteOperation { return operation.execute(client); } - - private String trimWebdavSuffix(String url) { - String trimmedUrl = url; - if (trimmedUrl == null) { - trimmedUrl = ""; - } else { - if (trimmedUrl.endsWith("/")) { - trimmedUrl = trimmedUrl.substring(0, trimmedUrl.length() - 1); - } - if (trimmedUrl.toLowerCase(Locale.ROOT).endsWith(AuthenticatorUrlUtils.WEBDAV_PATH_4_0_AND_LATER)) { - trimmedUrl = trimmedUrl.substring(0, - trimmedUrl.length() - AuthenticatorUrlUtils.WEBDAV_PATH_4_0_AND_LATER.length()); - } - } - return trimmedUrl; - } - - private String normalizeProtocolPrefix(String url, boolean isSslConn) { if (!url.toLowerCase(Locale.ROOT).startsWith("http://") && !url.toLowerCase(Locale.ROOT).startsWith("https://")) { diff --git a/app/src/main/java/com/owncloud/android/operations/GetSharesForFileOperation.java b/app/src/main/java/com/owncloud/android/operations/GetSharesForFileOperation.java deleted file mode 100644 index 2e9e18e..0000000 --- a/app/src/main/java/com/owncloud/android/operations/GetSharesForFileOperation.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2021 Tobias Kaminsky - * SPDX-FileCopyrightText: 2015 ownCloud Inc. - * SPDX-FileCopyrightText: 2014-2015 María Asensio Valverde - * SPDX-FileCopyrightText: 2015 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later - */ -package com.owncloud.android.operations; - -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.shares.GetSharesForFileRemoteOperation; -import com.owncloud.android.lib.resources.shares.OCShare; -import com.owncloud.android.operations.common.SyncOperation; - -import java.util.ArrayList; - -/** - * Provide a list shares for a specific file. - */ -public class GetSharesForFileOperation extends SyncOperation { - - private static final String TAG = GetSharesForFileOperation.class.getSimpleName(); - - private final String path; - private final boolean reshares; - private final boolean subfiles; - - /** - * Constructor - * - * @param path Path to file or folder - * @param reshares If set to false (default), only shares from the current user are returned If set to true, all - * shares from the given file are returned - * @param subfiles If set to false (default), lists only the folder being shared If set to true, all shared files - * within the folder are returned. - */ - public GetSharesForFileOperation(String path, - boolean reshares, - boolean subfiles, - FileDataStorageManager storageManager) { - super(storageManager); - - this.path = path; - this.reshares = reshares; - this.subfiles = subfiles; - } - - @Override - protected RemoteOperationResult run(OwnCloudClient client) { - GetSharesForFileRemoteOperation operation = new GetSharesForFileRemoteOperation(path, - reshares, - subfiles); - RemoteOperationResult result = operation.execute(client); - - if (result.isSuccess()) { - - // Update DB with the response - Log_OC.d(TAG, "File = " + path + " Share list size " + result.getData().size()); - ArrayList shares = new ArrayList(); - for (Object obj : result.getData()) { - shares.add((OCShare) obj); - } - - getStorageManager().saveSharesDB(shares); - - } else if (result.getCode() == RemoteOperationResult.ResultCode.SHARE_NOT_FOUND) { - // no share on the file - remove local shares - getStorageManager().removeSharesForFile(path); - - } - - return result; - } - -} diff --git a/app/src/main/java/com/owncloud/android/operations/GetSharesForFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/GetSharesForFileOperation.kt new file mode 100644 index 0000000..47634dd --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/GetSharesForFileOperation.kt @@ -0,0 +1,68 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-FileCopyrightText: 2021 Tobias Kaminsky + * SPDX-FileCopyrightText: 2015 ownCloud Inc. + * SPDX-FileCopyrightText: 2014-2015 María Asensio Valverde + * SPDX-FileCopyrightText: 2015 David A. Velasco + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) + */ +package com.owncloud.android.operations + +import com.nextcloud.android.lib.resources.files.GetFilesDownloadLimitRemoteOperation +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.shares.GetSharesForFileRemoteOperation +import com.owncloud.android.lib.resources.shares.OCShare +import com.owncloud.android.lib.resources.shares.ShareType +import com.owncloud.android.operations.common.SyncOperation + +/** + * Provide a list shares for a specific file. + */ +class GetSharesForFileOperation( + private val path: String, + private val reshares: Boolean, + private val subfiles: Boolean, + storageManager: FileDataStorageManager +) : SyncOperation(storageManager) { + + @Suppress("DEPRECATION", "NestedBlockDepth") + @Deprecated("Deprecated in Java") + override fun run(client: OwnCloudClient): RemoteOperationResult> { + val result = GetSharesForFileRemoteOperation(path, reshares, subfiles).execute(client) + + if (result.isSuccess) { + // Update DB with the response + val shares = result.resultData + Log_OC.d(TAG, "File = $path Share list size ${shares.size}") + + val capability = storageManager.getCapability(storageManager.user) + if (capability.filesDownloadLimit.isTrue && shares.any { it.shareType == ShareType.PUBLIC_LINK }) { + val downloadLimitResult = GetFilesDownloadLimitRemoteOperation(path, subfiles).execute(client) + if (downloadLimitResult.isSuccess) { + val downloadLimits = downloadLimitResult.resultData + downloadLimits.forEach { downloadLimit -> + shares.find { share -> + share.token == downloadLimit.token + }?.fileDownloadLimit = downloadLimit + } + } + } + + storageManager.saveSharesDB(shares) + } else if (result.code == RemoteOperationResult.ResultCode.SHARE_NOT_FOUND) { + // no share on the file - remove local shares + storageManager.removeSharesForFile(path) + } + + return result + } + + companion object { + private val TAG: String = GetSharesForFileOperation::class.java.simpleName + } +} diff --git a/app/src/main/java/com/owncloud/android/operations/GetUserProfileOperation.java b/app/src/main/java/com/owncloud/android/operations/GetUserProfileOperation.java index 4aa248d..48de76b 100644 --- a/app/src/main/java/com/owncloud/android/operations/GetUserProfileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/GetUserProfileOperation.java @@ -5,7 +5,7 @@ * SPDX-FileCopyrightText: 2021 Chris Narkiewicz * SPDX-FileCopyrightText: 2016 ownCloud Inc. * SPDX-FileCopyrightText: 2016 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations; diff --git a/app/src/main/java/com/owncloud/android/operations/MoveFileOperation.java b/app/src/main/java/com/owncloud/android/operations/MoveFileOperation.java index d50fe82..8e2d1d1 100644 --- a/app/src/main/java/com/owncloud/android/operations/MoveFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/MoveFileOperation.java @@ -6,7 +6,7 @@ * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2015 María Asensio Valverde * SPDX-FileCopyrightText: 2014 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations; diff --git a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java index e0f0538..66bd13a 100644 --- a/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/RefreshFolderOperation.java @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2019-2023 Tobias Kaminsky * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2013 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations; @@ -16,6 +16,7 @@ import com.google.gson.Gson; import com.nextcloud.android.lib.resources.directediting.DirectEditingObtainRemoteOperation; import com.nextcloud.client.account.User; import com.nextcloud.common.NextcloudClient; +import com.nextcloud.utils.extensions.StringExtensionsKt; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; import com.owncloud.android.datamodel.FileDataStorageManager; @@ -35,9 +36,6 @@ import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation; import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation; import com.owncloud.android.lib.resources.files.model.RemoteFile; -import com.owncloud.android.lib.resources.shares.GetSharesForFileRemoteOperation; -import com.owncloud.android.lib.resources.shares.OCShare; -import com.owncloud.android.lib.resources.shares.ShareType; import com.owncloud.android.lib.resources.status.E2EVersion; import com.owncloud.android.lib.resources.users.GetPredefinedStatusesRemoteOperation; import com.owncloud.android.lib.resources.users.PredefinedStatus; @@ -77,10 +75,12 @@ public class RefreshFolderOperation extends RemoteOperation { public static final String EVENT_SINGLE_FOLDER_SHARES_SYNCED = RefreshFolderOperation.class.getName() + ".EVENT_SINGLE_FOLDER_SHARES_SYNCED"; + private boolean isMetadataSyncWorkerRunning = false; + /** * Time stamp for the synchronization process in progress */ - private long mCurrentSyncTime; + private final long mCurrentSyncTime; /** * Remote folder to synchronize @@ -90,17 +90,17 @@ public class RefreshFolderOperation extends RemoteOperation { /** * Access to the local database */ - private FileDataStorageManager mStorageManager; + private final FileDataStorageManager fileDataStorageManager; /** * Account where the file to synchronize belongs */ - private User user; + private final User user; /** * Android context; necessary to send requests to the download service */ - private Context mContext; + private final Context mContext; /** * Files and folders contained in the synchronized folder after a successful operation @@ -121,12 +121,12 @@ public class RefreshFolderOperation extends RemoteOperation { * Map of remote and local paths to files that where locally stored in a location out of the ownCloud folder and * couldn't be copied automatically into it **/ - private Map mForgottenLocalFiles; + private final Map mForgottenLocalFiles; /** * 'True' means that this operation is part of a full account synchronization */ - private boolean mSyncFullAccount; + private final boolean mSyncFullAccount; /** * 'True' means that the remote folder changed and should be fetched @@ -136,14 +136,14 @@ public class RefreshFolderOperation extends RemoteOperation { /** * 'True' means that Etag will be ignored */ - private boolean mIgnoreETag; + private final boolean mIgnoreETag; /** * 'True' means that no share and no capabilities will be updated */ - private boolean mOnlyFileMetadata; + private final boolean mOnlyFileMetadata; - private List mFilesToSyncContents; + private final List mFilesToSyncContents; // this will be used for every file when 'folder synchronization' replaces 'folder download' @@ -169,7 +169,7 @@ public class RefreshFolderOperation extends RemoteOperation { mLocalFolder = folder; mCurrentSyncTime = currentSyncTime; mSyncFullAccount = syncFullAccount; - mStorageManager = dataStorageManager; + fileDataStorageManager = dataStorageManager; this.user = user; mContext = context; mForgottenLocalFiles = new HashMap<>(); @@ -179,6 +179,29 @@ public class RefreshFolderOperation extends RemoteOperation { mFilesToSyncContents = new Vector<>(); } + /** + * Returns RefreshFolderOperation for metadata sync worker + */ + public RefreshFolderOperation(OCFile folder, + FileDataStorageManager dataStorageManager, + User user, + Context context) { + mLocalFolder = folder; + mCurrentSyncTime = System.currentTimeMillis(); + mSyncFullAccount = false; + fileDataStorageManager = dataStorageManager; + this.user = user; + mContext = context; + mForgottenLocalFiles = new HashMap<>(); + mRemoteFolderChanged = false; + mIgnoreETag = false; + mOnlyFileMetadata = true; + mFilesToSyncContents = new Vector<>(); + + // since metadata worker working in background for sub-folders no need send folder refresh event + isMetadataSyncWorkerRunning = true; + } + public RefreshFolderOperation(OCFile folder, long currentSyncTime, boolean syncFullAccount, @@ -190,7 +213,7 @@ public class RefreshFolderOperation extends RemoteOperation { mLocalFolder = folder; mCurrentSyncTime = currentSyncTime; mSyncFullAccount = syncFullAccount; - mStorageManager = dataStorageManager; + fileDataStorageManager = dataStorageManager; this.user = user; mContext = context; mForgottenLocalFiles = new HashMap<>(); @@ -234,6 +257,11 @@ public class RefreshFolderOperation extends RemoteOperation { mConflictsFound = 0; mForgottenLocalFiles.clear(); + if (mLocalFolder == null) { + Log_OC.e(TAG, "Local folder is null, cannot run refresh folder operation"); + return new RemoteOperationResult<>(ResultCode.FILE_NOT_FOUND); + } + if (OCFile.ROOT_PATH.equals(mLocalFolder.getRemotePath()) && !mSyncFullAccount && !mOnlyFileMetadata) { updateOCVersion(client); updateUserProfile(); @@ -243,10 +271,10 @@ public class RefreshFolderOperation extends RemoteOperation { if (result.isSuccess()) { if (mRemoteFolderChanged) { - // TODO catch IllegalStateException, show properly to user result = fetchAndSyncRemoteFolder(client); } else { - mChildren = mStorageManager.getFolderContent(mLocalFolder, false); + Log_OC.d(TAG, "💾 Remote folder is not changed, getting folder content from database"); + mChildren = fileDataStorageManager.getFolderContent(mLocalFolder, false); } if (result.isSuccess()) { @@ -256,28 +284,36 @@ public class RefreshFolderOperation extends RemoteOperation { mLocalFolder.setEtag(""); } - mLocalFolder.setLastSyncDateForData(System.currentTimeMillis()); - mStorageManager.saveFile(mLocalFolder); + if (mLocalFolder != null) { + mLocalFolder.setLastSyncDateForData(System.currentTimeMillis()); + fileDataStorageManager.saveFile(mLocalFolder); + } else { + Log_OC.e(TAG, "Local folder is null, cannot set last sync date nor save file"); + result = new RemoteOperationResult<>(ResultCode.FILE_NOT_FOUND); + } } - if (!mSyncFullAccount && mRemoteFolderChanged) { - sendLocalBroadcast( - EVENT_SINGLE_FOLDER_CONTENTS_SYNCED, mLocalFolder.getRemotePath(), result - ); + if (!mSyncFullAccount && mRemoteFolderChanged && mLocalFolder != null && !isMetadataSyncWorkerRunning) { + sendLocalBroadcast(EVENT_SINGLE_FOLDER_CONTENTS_SYNCED, mLocalFolder.getRemotePath(), result); } - if (result.isSuccess() && !mSyncFullAccount && !mOnlyFileMetadata) { - refreshSharesForFolder(client); // share result is ignored + if (result.isSuccess() && result.getData() != null && !mSyncFullAccount && !mOnlyFileMetadata) { + final var remoteObject = result.getData(); + final ArrayList remoteFiles = new ArrayList<>(); + for (Object object: remoteObject) { + if (object instanceof RemoteFile remoteFile) { + remoteFiles.add(remoteFile); + } + } + + fileDataStorageManager.saveSharesFromRemoteFile(remoteFiles); } - if (!mSyncFullAccount) { - sendLocalBroadcast( - EVENT_SINGLE_FOLDER_SHARES_SYNCED, mLocalFolder.getRemotePath(), result - ); + if (!mSyncFullAccount && mLocalFolder != null && !isMetadataSyncWorkerRunning) { + sendLocalBroadcast(EVENT_SINGLE_FOLDER_SHARES_SYNCED, mLocalFolder.getRemotePath(), result); } return result; - } private void updateOCVersion(OwnCloudClient client) { @@ -293,7 +329,7 @@ public class RefreshFolderOperation extends RemoteOperation { try { NextcloudClient nextcloudClient = OwnCloudClientFactory.createNextcloudClient(user, mContext); - RemoteOperationResult result = new GetUserProfileOperation(mStorageManager).execute(nextcloudClient); + RemoteOperationResult result = new GetUserProfileOperation(fileDataStorageManager).execute(nextcloudClient); if (!result.isSuccess()) { Log_OC.w(TAG, "Couldn't update user profile from server"); } else { @@ -309,9 +345,9 @@ public class RefreshFolderOperation extends RemoteOperation { String oldDirectEditingEtag = arbitraryDataProvider.getValue(user, ArbitraryDataProvider.DIRECT_EDITING_ETAG); - RemoteOperationResult result = new GetCapabilitiesOperation(mStorageManager).execute(mContext); + RemoteOperationResult result = new GetCapabilitiesOperation(fileDataStorageManager).execute(mContext); if (result.isSuccess()) { - String newDirectEditingEtag = mStorageManager.getCapability(user.getAccountName()).getDirectEditingEtag(); + String newDirectEditingEtag = fileDataStorageManager.getCapability(user.getAccountName()).getDirectEditingEtag(); if (!oldDirectEditingEtag.equalsIgnoreCase(newDirectEditingEtag)) { updateDirectEditing(arbitraryDataProvider, newDirectEditingEtag); @@ -324,8 +360,8 @@ public class RefreshFolderOperation extends RemoteOperation { } private void updateDirectEditing(ArbitraryDataProvider arbitraryDataProvider, String newDirectEditingEtag) { - RemoteOperationResult result = new DirectEditingObtainRemoteOperation().execute(user, - mContext); + RemoteOperationResult result = + new DirectEditingObtainRemoteOperation().executeNextcloudClient(user, mContext); if (result.isSuccess()) { DirectEditing directEditing = result.getResultData(); @@ -364,7 +400,12 @@ public class RefreshFolderOperation extends RemoteOperation { private RemoteOperationResult checkForChanges(OwnCloudClient client) { mRemoteFolderChanged = true; - RemoteOperationResult result; + if (isMetadataSyncWorkerRunning) { + Log_OC.d(TAG, "Skipping eTag check since metadata worker already did"); + return new RemoteOperationResult<>(ResultCode.OK); + } + + RemoteOperationResult result; String remotePath = mLocalFolder.getRemotePath(); Log_OC.d(TAG, "Checking changes in " + user.getAccountName() + remotePath); @@ -373,19 +414,28 @@ public class RefreshFolderOperation extends RemoteOperation { result = new ReadFileRemoteOperation(remotePath).execute(client); if (result.isSuccess()) { - OCFile remoteFolder = FileStorageUtils.fillOCFile((RemoteFile) result.getData().get(0)); - - if (!mIgnoreETag) { + if (!mIgnoreETag && result.getData().get(0) instanceof RemoteFile remoteFile) { // check if remote and local folder are different - String remoteFolderETag = remoteFolder.getEtag(); + String remoteFolderETag = remoteFile.getEtag(); if (remoteFolderETag != null) { - mRemoteFolderChanged = !(remoteFolderETag.equalsIgnoreCase(mLocalFolder.getEtag())); + String localFolderEtag = mLocalFolder.getEtag(); + mRemoteFolderChanged = !StringExtensionsKt.isNotBlankAndEquals(remoteFolderETag, localFolderEtag); + Log_OC.d( + TAG, + "📂 eTag check\n" + + " Path: " + remoteFile.getRemotePath() + "\n" + + " Local eTag: " + localFolderEtag + "\n" + + " Remote eTag: " + remoteFolderETag + "\n" + + " Changed: " + mRemoteFolderChanged + ); } else { Log_OC.e(TAG, "Checked " + user.getAccountName() + remotePath + ": No ETag received from server"); } + } else { + Log_OC.d(TAG, "Ignoring eTag. mRemoteFolderChanged is true."); } - result = new RemoteOperationResult(ResultCode.OK); + result = new RemoteOperationResult<>(ResultCode.OK); Log_OC.i(TAG, "Checked " + user.getAccountName() + remotePath + " : " + (mRemoteFolderChanged ? "changed" : "not changed")); @@ -407,12 +457,10 @@ public class RefreshFolderOperation extends RemoteOperation { return result; } - private RemoteOperationResult fetchAndSyncRemoteFolder(OwnCloudClient client) { String remotePath = mLocalFolder.getRemotePath(); RemoteOperationResult result = new ReadFolderRemoteOperation(remotePath).execute(client); - Log_OC.d(TAG, "Refresh folder " + user.getAccountName() + remotePath); - Log_OC.d(TAG, "Refresh folder with remote id" + mLocalFolder.getRemoteId()); + Log_OC.d(TAG, "⬇ eTag is changed or ignored, fetching folder: " + user.getAccountName() + remotePath); if (result.isSuccess()) { synchronizeData(result.getData()); @@ -430,13 +478,13 @@ public class RefreshFolderOperation extends RemoteOperation { } private void removeLocalFolder() { - if (mStorageManager.fileExists(mLocalFolder.getFileId())) { + if (fileDataStorageManager.fileExists(mLocalFolder.getFileId())) { String currentSavePath = FileStorageUtils.getSavePath(user.getAccountName()); - mStorageManager.removeFolder( + fileDataStorageManager.removeFolder( mLocalFolder, true, mLocalFolder.isDown() && mLocalFolder.getStoragePath().startsWith(currentSavePath) - ); + ); } } @@ -451,7 +499,7 @@ public class RefreshFolderOperation extends RemoteOperation { */ private void synchronizeData(List folderAndFiles) { // get 'fresh data' from the database - mLocalFolder = mStorageManager.getFileByPath(mLocalFolder.getRemotePath()); + mLocalFolder = fileDataStorageManager.getFileByPath(mLocalFolder.getRemotePath()); if (mLocalFolder == null) { Log_OC.d(TAG,"mLocalFolder cannot be null"); @@ -469,7 +517,7 @@ public class RefreshFolderOperation extends RemoteOperation { mFilesToSyncContents.clear(); // if local folder is encrypted, download fresh metadata - boolean encryptedAncestor = FileStorageUtils.checkEncryptionStatus(mLocalFolder, mStorageManager); + boolean encryptedAncestor = FileStorageUtils.checkEncryptionStatus(mLocalFolder, fileDataStorageManager); mLocalFolder.setEncrypted(encryptedAncestor); // update permission @@ -502,14 +550,12 @@ public class RefreshFolderOperation extends RemoteOperation { // get current data about local contents of the folder to synchronize Map localFilesMap; E2EVersion e2EVersion; - if (object instanceof DecryptedFolderMetadataFileV1) { + if (object instanceof DecryptedFolderMetadataFileV1 metadataFileV1) { e2EVersion = E2EVersion.V1_2; - localFilesMap = prefillLocalFilesMap((DecryptedFolderMetadataFileV1) object, - mStorageManager.getFolderContent(mLocalFolder, false)); + localFilesMap = prefillLocalFilesMap(metadataFileV1, fileDataStorageManager.getFolderContent(mLocalFolder, false)); } else { e2EVersion = E2EVersion.V2_0; - localFilesMap = prefillLocalFilesMap((DecryptedFolderMetadataFile) object, - mStorageManager.getFolderContent(mLocalFolder, false)); + localFilesMap = prefillLocalFilesMap(object, fileDataStorageManager.getFolderContent(mLocalFolder, false)); // update counter if (object != null) { @@ -537,7 +583,7 @@ public class RefreshFolderOperation extends RemoteOperation { // TODO better implementation is needed if (localFile == null) { - localFile = mStorageManager.getFileByPath(updatedFile.getRemotePath()); + localFile = fileDataStorageManager.getFileByPath(updatedFile.getRemotePath()); } // add to updatedFile data about LOCAL STATE (not existing in server) @@ -556,11 +602,11 @@ public class RefreshFolderOperation extends RemoteOperation { // update file name for encrypted files if (e2EVersion == E2EVersion.V1_2) { - updateFileNameForEncryptedFileV1(mStorageManager, + updateFileNameForEncryptedFileV1(fileDataStorageManager, (DecryptedFolderMetadataFileV1) object, updatedFile); - } else { - updateFileNameForEncryptedFile(mStorageManager, + } else if (object != null) { + updateFileNameForEncryptedFile(fileDataStorageManager, (DecryptedFolderMetadataFile) object, updatedFile); if (localFile != null) { @@ -579,15 +625,15 @@ public class RefreshFolderOperation extends RemoteOperation { // save updated contents in local database // update file name for encrypted files if (e2EVersion == E2EVersion.V1_2) { - updateFileNameForEncryptedFileV1(mStorageManager, + updateFileNameForEncryptedFileV1(fileDataStorageManager, (DecryptedFolderMetadataFileV1) object, mLocalFolder); } else { - updateFileNameForEncryptedFile(mStorageManager, + updateFileNameForEncryptedFile(fileDataStorageManager, (DecryptedFolderMetadataFile) object, mLocalFolder); } - mStorageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values()); + fileDataStorageManager.saveFolder(remoteFolder, updatedFiles, localFilesMap.values()); mChildren = updatedFiles; } @@ -697,6 +743,7 @@ public class RefreshFolderOperation extends RemoteOperation { if (localFile != null) { updatedFile.setFileId(localFile.getFileId()); updatedFile.setLastSyncDateForData(localFile.getLastSyncDateForData()); + updatedFile.setInternalFolderSyncTimestamp(localFile.getInternalFolderSyncTimestamp()); updatedFile.setModificationTimestampAtLastSyncForData( localFile.getModificationTimestampAtLastSyncForData() ); @@ -790,38 +837,6 @@ public class RefreshFolderOperation extends RemoteOperation { } } - /** - * Syncs the Share resources for the files contained in the folder refreshed (children, not deeper descendants). - * - * @param client Handler of a session with an OC server. - * @return The result of the remote operation retrieving the Share resources in the folder refreshed by the - * operation. - */ - private RemoteOperationResult refreshSharesForFolder(OwnCloudClient client) { - RemoteOperationResult result; - - // remote request - GetSharesForFileRemoteOperation operation = - new GetSharesForFileRemoteOperation(mLocalFolder.getRemotePath(), true, true); - result = operation.execute(client); - - if (result.isSuccess()) { - // update local database - ArrayList shares = new ArrayList<>(); - OCShare share; - for (Object obj : result.getData()) { - share = (OCShare) obj; - - if (ShareType.NO_SHARED != share.getShareType()) { - shares.add(share); - } - } - mStorageManager.saveSharesInFolder(shares, mLocalFolder); - } - - return result; - } - /** * Sends a message to any application component interested in the progress of the synchronization. * diff --git a/app/src/main/java/com/owncloud/android/operations/RemoteOperationFailedException.java b/app/src/main/java/com/owncloud/android/operations/RemoteOperationFailedException.java index d52e798..c797105 100644 --- a/app/src/main/java/com/owncloud/android/operations/RemoteOperationFailedException.java +++ b/app/src/main/java/com/owncloud/android/operations/RemoteOperationFailedException.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2017-2019 Andy Scherzinger - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.operations; diff --git a/app/src/main/java/com/owncloud/android/operations/RemoveFileOperation.java b/app/src/main/java/com/owncloud/android/operations/RemoveFileOperation.java deleted file mode 100644 index ab3239b..0000000 --- a/app/src/main/java/com/owncloud/android/operations/RemoveFileOperation.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2015 ownCloud Inc. - * SPDX-FileCopyrightText: 2015 María Asensio Valverde - * SPDX-FileCopyrightText: 2012 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later - */ -package com.owncloud.android.operations; - -import android.content.Context; - -import com.nextcloud.client.account.User; -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.datamodel.ThumbnailsCacheManager; -import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.operations.RemoteOperation; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; -import com.owncloud.android.lib.resources.files.RemoveFileRemoteOperation; -import com.owncloud.android.operations.common.SyncOperation; -import com.owncloud.android.utils.MimeTypeUtil; - -/** - * Remote operation performing the removal of a remote file or folder in the ownCloud server. - */ -public class RemoveFileOperation extends SyncOperation { - - private final OCFile fileToRemove; - private final boolean onlyLocalCopy; - private final User user; - private final boolean inBackground; - private final Context context; - - /** - * Constructor - * - * @param fileToRemove OCFile instance describing the remote file or folder to remove from the server - * @param onlyLocalCopy When 'true', and a local copy of the file exists, only this is removed. - */ - public RemoveFileOperation(OCFile fileToRemove, - boolean onlyLocalCopy, - User user, - boolean inBackground, - Context context, - FileDataStorageManager storageManager) { - super(storageManager); - - this.fileToRemove = fileToRemove; - this.onlyLocalCopy = onlyLocalCopy; - this.user = user; - this.inBackground = inBackground; - this.context = context; - } - - /** - * Getter for the file to remove (or removed, if the operation was successfully performed). - * - * @return File to remove or already removed. - */ - public OCFile getFile() { - return fileToRemove; - } - - public boolean isInBackground() { - return inBackground; - } - - /** - * Performs the remove operation - * - * @param client Client object to communicate with the remote ownCloud server. - */ - @Override - protected RemoteOperationResult run(OwnCloudClient client) { - RemoteOperationResult result = null; - RemoteOperation operation; - - if (MimeTypeUtil.isImage(fileToRemove.getMimeType())) { - // store resized image - ThumbnailsCacheManager.generateResizedImage(fileToRemove); - } - - boolean localRemovalFailed = false; - if (!onlyLocalCopy) { - if (fileToRemove.isEncrypted()) { - OCFile parent = getStorageManager().getFileByPath(fileToRemove.getParentRemotePath()); - operation = new RemoveRemoteEncryptedFileOperation(fileToRemove.getRemotePath(), - user, - context, - fileToRemove.getEncryptedFileName(), - parent, - fileToRemove.isFolder()); - } else { - operation = new RemoveFileRemoteOperation(fileToRemove.getRemotePath()); - } - result = operation.execute(client); - if (result.isSuccess() || result.getCode() == ResultCode.FILE_NOT_FOUND) { - localRemovalFailed = !(getStorageManager().removeFile(fileToRemove, true, true)); - } - } else { - localRemovalFailed = !(getStorageManager().removeFile(fileToRemove, false, true)); - if (!localRemovalFailed) { - result = new RemoteOperationResult(ResultCode.OK); - } - } - - if (localRemovalFailed) { - result = new RemoteOperationResult(ResultCode.LOCAL_STORAGE_NOT_REMOVED); - } - - return result; - } -} diff --git a/app/src/main/java/com/owncloud/android/operations/RemoveFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/RemoveFileOperation.kt new file mode 100644 index 0000000..d1fd55d --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/RemoveFileOperation.kt @@ -0,0 +1,99 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-FileCopyrightText: 2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2015 ownCloud Inc. + * SPDX-FileCopyrightText: 2015 María Asensio Valverde + * SPDX-FileCopyrightText: 2012 David A. Velasco + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) + */ +package com.owncloud.android.operations +import android.content.Context +import com.nextcloud.client.account.User +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.ThumbnailsCacheManager +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.resources.files.RemoveFileRemoteOperation +import com.owncloud.android.operations.common.SyncOperation +import com.owncloud.android.utils.MimeTypeUtil + +/** + * Remote operation to remove a remote file or folder from an ownCloud server. + * + * @param file OCFile instance representing the remote file or folder to remove. + * @param onlyLocalCopy If true, only the local copy will be removed (if it exists). + * @param user User account associated with the operation. + * @param isInBackground Flag indicating if the operation runs in the background. + * @param context Android context. + * @param storageManager Storage manager handling local file operations. + */ +@Suppress("LongParameterList") +class RemoveFileOperation( + val file: OCFile, + private val onlyLocalCopy: Boolean, + private val user: User, + val isInBackground: Boolean, + private val context: Context, + storageManager: FileDataStorageManager +) : SyncOperation(storageManager) { + + /** + * Executes the remove operation. + * + * If the file is an image, it will also be removed from the thumbnail cache. + * Handles both encrypted and non-encrypted files. Removes the file locally if needed. + * + * @param client OwnCloudClient used to communicate with the remote server. + * @return RemoteOperationResult indicating success or failure of the operation. + */ + override fun run(client: OwnCloudClient?): RemoteOperationResult<*> { + var result: RemoteOperationResult<*>? = null + val operation: RemoteOperation<*>? + + var localRemovalFailed = false + + if (onlyLocalCopy) { + // generate resize image if image is deleted only locally, to save server request + if (MimeTypeUtil.isImage(file.mimeType)) { + ThumbnailsCacheManager.generateResizedImage(file) + } + + localRemovalFailed = !storageManager.removeFile(file, false, true) + if (!localRemovalFailed) { + result = RemoteOperationResult(RemoteOperationResult.ResultCode.OK) + } + } else { + operation = if (file.isEncrypted) { + val parent = storageManager.getFileById(file.parentId) + if (parent == null) { + return RemoteOperationResult(RemoteOperationResult.ResultCode.LOCAL_FILE_NOT_FOUND) + } + RemoveRemoteEncryptedFileOperation( + file.remotePath, + user, + context, + file.getEncryptedFileName(), + parent, + file.isFolder + ) + } else { + RemoveFileRemoteOperation(file.remotePath) + } + + result = operation.execute(client) + if (result.isSuccess || result.code == RemoteOperationResult.ResultCode.FILE_NOT_FOUND) { + localRemovalFailed = !storageManager.removeFile(file, true, true) + } + } + + if (localRemovalFailed) { + result = RemoteOperationResult(RemoteOperationResult.ResultCode.LOCAL_STORAGE_NOT_REMOVED) + } + + return result ?: RemoteOperationResult(RemoteOperationResult.ResultCode.CANCELLED) + } +} diff --git a/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt b/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt index 6248d1a..5c85d0a 100644 --- a/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt +++ b/app/src/main/java/com/owncloud/android/operations/RemoveRemoteEncryptedFileOperation.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2017 Tobias Kaminsky * SPDX-FileCopyrightText: 2017 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.operations @@ -52,7 +52,7 @@ class RemoveRemoteEncryptedFileOperation internal constructor( @Deprecated("Deprecated in Java") @Suppress("TooGenericExceptionCaught") override fun run(client: OwnCloudClient): RemoteOperationResult { - val result: RemoteOperationResult + var result: RemoteOperationResult var delete: DeleteMethod? = null var token: String? = null val e2eVersion = CapabilityUtils.getCapability(context).endToEndEncryptionApiVersion diff --git a/app/src/main/java/com/owncloud/android/operations/RenameFileOperation.java b/app/src/main/java/com/owncloud/android/operations/RenameFileOperation.java index 2fb1b9a..93e60b1 100644 --- a/app/src/main/java/com/owncloud/android/operations/RenameFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/RenameFileOperation.java @@ -6,7 +6,7 @@ * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2015 María Asensio Valverde * SPDX-FileCopyrightText: 2012 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations; @@ -25,6 +25,9 @@ import com.owncloud.android.utils.MimeTypeUtil; import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; /** * Remote operation performing the rename of a remote file (or folder?) in the ownCloud server. @@ -161,26 +164,32 @@ public class RenameFileOperation extends SyncOperation { } // create a test file String tmpFolderName = FileStorageUtils.getTemporalPath(""); - File testFile = new File(tmpFolderName + newName); - File tmpFolder = testFile.getParentFile(); - if (!tmpFolder.exists() && !tmpFolder.mkdirs()) { - Log_OC.e(TAG, "Unable to create parent folder " + tmpFolder.getAbsolutePath()); + Path testFile = Paths.get(tmpFolderName, newName); + Path tmpFolder = testFile.getParent(); + if (tmpFolder != null && !Files.exists(tmpFolder)) { + try { + Files.createDirectories(tmpFolder); + } catch (IOException e) { + Log_OC.e(TAG, "Unable to create parent folder " + tmpFolder.toAbsolutePath()); + } } - if (!tmpFolder.isDirectory()) { + if (tmpFolder != null && !Files.isDirectory(tmpFolder)) { throw new IOException("Unexpected error: temporal directory could not be created"); } try { - testFile.createNewFile(); // return value is ignored; it could be 'false' because - // the file already existed, that doesn't invalidate the name - } catch (IOException e) { + Files.createFile(testFile); + } catch (Exception e) { Log_OC.i(TAG, "Test for validity of name " + newName + " in the file system failed"); return false; } - boolean result = testFile.exists() && testFile.isFile(); + boolean result = Files.exists(testFile) && Files.isRegularFile(testFile); - // cleaning ; result is ignored, since there is not much we could do in case of failure, - // but repeat and repeat... - testFile.delete(); + try { + Files.deleteIfExists(testFile); + } catch (Exception e) { + Log_OC.e("Error deleting file: ", e.getMessage()); + return true; + } return result; } diff --git a/app/src/main/java/com/owncloud/android/operations/RichDocumentsCreateAssetOperation.java b/app/src/main/java/com/owncloud/android/operations/RichDocumentsCreateAssetOperation.java index 2fe795a..e593a20 100644 --- a/app/src/main/java/com/owncloud/android/operations/RichDocumentsCreateAssetOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/RichDocumentsCreateAssetOperation.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2018 Tobias Kaminsky * SPDX-FileCopyrightText: 2018 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.operations; diff --git a/app/src/main/java/com/owncloud/android/operations/RichDocumentsUrlOperation.java b/app/src/main/java/com/owncloud/android/operations/RichDocumentsUrlOperation.java index a236948..a05a892 100644 --- a/app/src/main/java/com/owncloud/android/operations/RichDocumentsUrlOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/RichDocumentsUrlOperation.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2018 Tobias Kaminsky * SPDX-FileCopyrightText: 2018 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.operations; diff --git a/app/src/main/java/com/owncloud/android/operations/SetFilesDownloadLimitOperation.kt b/app/src/main/java/com/owncloud/android/operations/SetFilesDownloadLimitOperation.kt new file mode 100644 index 0000000..8918172 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/SetFilesDownloadLimitOperation.kt @@ -0,0 +1,55 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 ZetaTom <70907959+zetatom@users.noreply.github.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.operations + +import android.content.Context +import com.nextcloud.android.lib.resources.files.GetFilesDownloadLimitRemoteOperation +import com.nextcloud.android.lib.resources.files.RemoveFilesDownloadLimitRemoteOperation +import com.nextcloud.android.lib.resources.files.SetFilesDownloadLimitRemoteOperation +import com.nextcloud.utils.extensions.toNextcloudClient +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult + +class SetFilesDownloadLimitOperation( + private val shareId: Long, + private val newLimit: Int, + private val fileDataStorageManager: FileDataStorageManager, + private val context: Context +) : RemoteOperation() { + @Deprecated("Deprecated in Java") + override fun run(client: OwnCloudClient): RemoteOperationResult { + val nextcloudClient = client.toNextcloudClient(context) + val share = fileDataStorageManager.getShareById(shareId) + val token = share?.token ?: return RemoteOperationResult(RemoteOperationResult.ResultCode.SHARE_NOT_FOUND) + + val result = if (newLimit > 0) { + val operation = SetFilesDownloadLimitRemoteOperation(token, newLimit) + nextcloudClient.execute(operation) + } else { + val operation = RemoveFilesDownloadLimitRemoteOperation(token) + nextcloudClient.execute(operation) + } + + val path = share.path + if (result.isSuccess && path != null) { + val getFilesDownloadLimitRemoteOperation = GetFilesDownloadLimitRemoteOperation(path, false) + val remoteOperationResult = getFilesDownloadLimitRemoteOperation.execute(client) + + if (remoteOperationResult.isSuccess) { + share.fileDownloadLimit = remoteOperationResult.resultData.firstOrNull { updatedDownloadLimit -> + updatedDownloadLimit.token == share.token + } + fileDataStorageManager.saveShare(share) + } + } + + return result + } +} diff --git a/app/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java b/app/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java index b141136..4aba8f2 100644 --- a/app/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java @@ -1,7 +1,7 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2021 Chris Narkiewicz * SPDX-FileCopyrightText: 2021 Tobias Kaminsky * SPDX-FileCopyrightText: 2016-2018 Andy Scherzinger @@ -9,7 +9,7 @@ * SPDX-FileCopyrightText: 2013-2016 María Asensio Valverde * SPDX-FileCopyrightText: 2012 David A. Velasco * SPDX-FileCopyrightText: 2012 Bartek Przybylski - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations; @@ -30,8 +30,12 @@ import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation; import com.owncloud.android.lib.resources.files.model.RemoteFile; import com.owncloud.android.operations.common.SyncOperation; +import com.owncloud.android.ui.events.DialogEvent; +import com.owncloud.android.ui.events.DialogEventType; import com.owncloud.android.utils.FileStorageUtils; +import org.greenrobot.eventbus.EventBus; + /** * Remote operation performing the read of remote file in the ownCloud server. */ @@ -46,6 +50,9 @@ public class SynchronizeFileOperation extends SyncOperation { private boolean mSyncFileContents; private Context mContext; private boolean mTransferWasRequested; + private final boolean syncInBackgroundWorker; + private boolean postDialogEvent = true; + /** * When 'false', uploads to the server are not done; only downloads or conflict detection. This is a temporal @@ -74,7 +81,9 @@ public class SynchronizeFileOperation extends SyncOperation { User user, boolean syncFileContents, Context context, - FileDataStorageManager storageManager) { + FileDataStorageManager storageManager, + boolean syncInBackgroundWorker, + boolean postDialogEvent) { super(storageManager); mRemotePath = remotePath; @@ -84,6 +93,8 @@ public class SynchronizeFileOperation extends SyncOperation { mSyncFileContents = syncFileContents; mContext = context; mAllowUploads = true; + this.syncInBackgroundWorker = syncInBackgroundWorker; + this.postDialogEvent = postDialogEvent; } @@ -110,7 +121,8 @@ public class SynchronizeFileOperation extends SyncOperation { User user, boolean syncFileContents, Context context, - FileDataStorageManager storageManager) { + FileDataStorageManager storageManager, + boolean syncInBackgroundWorker) { super(storageManager); mLocalFile = localFile; @@ -130,6 +142,7 @@ public class SynchronizeFileOperation extends SyncOperation { mSyncFileContents = syncFileContents; mContext = context; mAllowUploads = true; + this.syncInBackgroundWorker = syncInBackgroundWorker; } @@ -159,9 +172,9 @@ public class SynchronizeFileOperation extends SyncOperation { boolean syncFileContents, boolean allowUploads, Context context, - FileDataStorageManager storageManager) { - - this(localFile, serverFile, user, syncFileContents, context, storageManager); + FileDataStorageManager storageManager, + boolean syncInBackgroundWorker) { + this(localFile, serverFile, user, syncFileContents, context, storageManager, syncInBackgroundWorker); mAllowUploads = allowUploads; } @@ -275,6 +288,9 @@ public class SynchronizeFileOperation extends SyncOperation { Log_OC.i(TAG, "Synchronizing " + mUser.getAccountName() + ", file " + mLocalFile.getRemotePath() + ": " + result.getLogMessage()); + if (postDialogEvent) { + EventBus.getDefault().post(new DialogEvent(DialogEventType.SYNC)); + } return result; } @@ -295,11 +311,32 @@ public class SynchronizeFileOperation extends SyncOperation { } private void requestForDownload(OCFile file) { - FileDownloadHelper.Companion.instance().downloadFile( - mUser, - file); + final var fileDownloadHelper = FileDownloadHelper.Companion.instance(); + final var filename = file.getFileName(); - mTransferWasRequested = true; + if (syncInBackgroundWorker) { + Log_OC.d(TAG, "downloading file without notification: " + filename); + + try { + final var operation = new DownloadFileOperation(mUser, file, mContext); + final var result = operation.execute(getClient()); + + mTransferWasRequested = true; + + if (result.isSuccess()) { + fileDownloadHelper.saveFile(file, operation, getStorageManager()); + Log_OC.d(TAG, "requestForDownload completed for: " + filename); + } else { + Log_OC.d(TAG, "requestForDownload failed for: " + filename); + } + } catch (Exception e) { + Log_OC.d(TAG, "Exception caught at requestForDownload" + e); + } + } else { + Log_OC.d(TAG, "downloading file with notification: " + filename); + mTransferWasRequested = true; + fileDownloadHelper.downloadFile(mUser, file); + } } public boolean transferWasRequested() { diff --git a/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java index f944977..2b58296 100644 --- a/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java @@ -6,7 +6,7 @@ * SPDX-FileCopyrightText: 2018 Andy Scherzinger * SPDX-FileCopyrightText: 2016 ownCloud Inc. * SPDX-FileCopyrightText: 2012-2013 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations; @@ -28,7 +28,6 @@ import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation; import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation; import com.owncloud.android.lib.resources.files.model.RemoteFile; -import com.owncloud.android.lib.resources.status.E2EVersion; import com.owncloud.android.operations.common.SyncOperation; import com.owncloud.android.services.OperationsService; import com.owncloud.android.utils.FileStorageUtils; @@ -55,9 +54,6 @@ public class SynchronizeFolderOperation extends SyncOperation { private static final String TAG = SynchronizeFolderOperation.class.getSimpleName(); - /** Time stamp for the synchronization process in progress */ - private long mCurrentSyncTime; - /** Remote path of the folder to synchronize */ private String mRemotePath; @@ -89,29 +85,30 @@ public class SynchronizeFolderOperation extends SyncOperation { private final AtomicBoolean mCancellationRequested; + private final boolean syncInBackgroundWorker; + /** * Creates a new instance of {@link SynchronizeFolderOperation}. * * @param context Application context. * @param remotePath Path to synchronize. * @param user Nextcloud account where the folder is located. - * @param currentSyncTime Time stamp for the synchronization process in progress. */ public SynchronizeFolderOperation(Context context, String remotePath, User user, - long currentSyncTime, - FileDataStorageManager storageManager) { + FileDataStorageManager storageManager, + boolean syncInBackgroundWorker) { super(storageManager); mRemotePath = remotePath; - mCurrentSyncTime = currentSyncTime; this.user = user; mContext = context; mRemoteFolderChanged = false; mFilesForDirectDownload = new Vector<>(); mFilesToSyncContents = new Vector<>(); mCancellationRequested = new AtomicBoolean(false); + this.syncInBackgroundWorker = syncInBackgroundWorker; } @@ -135,13 +132,12 @@ public class SynchronizeFolderOperation extends SyncOperation { if (result.isSuccess()) { if (mRemoteFolderChanged) { result = fetchAndSyncRemoteFolder(client); - } else { prepareOpsFromLocalKnowledge(); } if (result.isSuccess()) { - syncContents(); + syncContents(client); } } @@ -283,18 +279,7 @@ public class SynchronizeFolderOperation extends SyncOperation { } // get current data about local contents of the folder to synchronize - Map localFilesMap; - E2EVersion e2EVersion; - - if (object instanceof DecryptedFolderMetadataFileV1) { - e2EVersion = E2EVersion.V1_2; - localFilesMap = RefreshFolderOperation.prefillLocalFilesMap((DecryptedFolderMetadataFileV1) object, - storageManager.getFolderContent(mLocalFolder, false)); - } else { - e2EVersion = E2EVersion.V2_0; - localFilesMap = RefreshFolderOperation.prefillLocalFilesMap((DecryptedFolderMetadataFile) object, - storageManager.getFolderContent(mLocalFolder, false)); - } + Map localFilesMap = RefreshFolderOperation.prefillLocalFilesMap(object,storageManager.getFolderContent(mLocalFolder, false)); // loop to synchronize every child List updatedFiles = new ArrayList<>(folderAndFiles.size() - 1); @@ -327,14 +312,10 @@ public class SynchronizeFolderOperation extends SyncOperation { FileStorageUtils.searchForLocalFileInDefaultPath(updatedFile, user.getAccountName()); // update file name for encrypted files - if (e2EVersion == E2EVersion.V1_2) { - RefreshFolderOperation.updateFileNameForEncryptedFileV1(storageManager, - (DecryptedFolderMetadataFileV1) object, - updatedFile); - } else { - RefreshFolderOperation.updateFileNameForEncryptedFile(storageManager, - (DecryptedFolderMetadataFile) object, - updatedFile); + if (object instanceof DecryptedFolderMetadataFileV1 metadataFile) { + RefreshFolderOperation.updateFileNameForEncryptedFileV1(storageManager, metadataFile, updatedFile); + } else if (object instanceof DecryptedFolderMetadataFile metadataFile) { + RefreshFolderOperation.updateFileNameForEncryptedFile(storageManager, metadataFile, updatedFile); } // we parse content, so either the folder itself or its direct parent (which we check) must be encrypted @@ -348,14 +329,10 @@ public class SynchronizeFolderOperation extends SyncOperation { } // update file name for encrypted files - if (e2EVersion == E2EVersion.V1_2) { - RefreshFolderOperation.updateFileNameForEncryptedFileV1(storageManager, - (DecryptedFolderMetadataFileV1) object, - mLocalFolder); - } else { - RefreshFolderOperation.updateFileNameForEncryptedFile(storageManager, - (DecryptedFolderMetadataFile) object, - mLocalFolder); + if (object instanceof DecryptedFolderMetadataFileV1 metadataFile) { + RefreshFolderOperation.updateFileNameForEncryptedFileV1(storageManager, metadataFile, mLocalFolder); + } else if (object instanceof DecryptedFolderMetadataFile metadataFile) { + RefreshFolderOperation.updateFileNameForEncryptedFile(storageManager, metadataFile, mLocalFolder); } // save updated contents in local database @@ -365,7 +342,7 @@ public class SynchronizeFolderOperation extends SyncOperation { } private void updateLocalStateData(OCFile remoteFile, OCFile localFile, OCFile updatedFile) { - updatedFile.setLastSyncDateForProperties(mCurrentSyncTime); + updatedFile.setLastSyncDateForProperties(System.currentTimeMillis()); if (localFile != null) { updatedFile.setFileId(localFile.getFileId()); updatedFile.setLastSyncDateForData(localFile.getLastSyncDateForData()); @@ -393,15 +370,27 @@ public class SynchronizeFolderOperation extends SyncOperation { } } - private void classifyFileForLaterSyncOrDownload(OCFile remoteFile, OCFile localFile) { - if (!remoteFile.isFolder()) { + @SuppressFBWarnings("JLM") + private void classifyFileForLaterSyncOrDownload(OCFile remoteFile, OCFile localFile) throws OperationCancelledException { + if (remoteFile.isFolder()) { + /// to download children files recursively + synchronized (mCancellationRequested) { + if (mCancellationRequested.get()) { + throw new OperationCancelledException(); + } + startSyncFolderOperation(remoteFile.getRemotePath()); + } + + } else { + /// prepare content synchronization for files (any file, not just favorites) SynchronizeFileOperation operation = new SynchronizeFileOperation( localFile, remoteFile, user, true, mContext, - getStorageManager() + getStorageManager(), + syncInBackgroundWorker ); mFilesToSyncContents.add(operation); } @@ -414,7 +403,6 @@ public class SynchronizeFolderOperation extends SyncOperation { if (!child.isFolder()) { if (!child.isDown()) { mFilesForDirectDownload.add(child); - } else { /// this should result in direct upload of files that were locally modified SynchronizeFileOperation operation = new SynchronizeFileOperation( @@ -423,23 +411,81 @@ public class SynchronizeFolderOperation extends SyncOperation { user, true, mContext, - getStorageManager() + getStorageManager(), + syncInBackgroundWorker ); mFilesToSyncContents.add(operation); - } } } } - private void syncContents() throws OperationCancelledException { + private void syncContents(OwnCloudClient client) throws OperationCancelledException { startDirectDownloads(); startContentSynchronizations(mFilesToSyncContents); + updateETag(client); } + /** + * Updates the eTag of the local folder after a successful synchronization. + * This ensures that any changes to local files, which may alter the eTag, are correctly reflected. + * + * @param client the OwnCloudClient instance used to execute remote operations. + */ + private void updateETag(OwnCloudClient client) { + ReadFolderRemoteOperation operation = new ReadFolderRemoteOperation(mRemotePath); + final var result = operation.execute(client); + if (!result.isSuccess()) { + Log_OC.w(TAG, "Cannot update eTag, read folder operation is failed"); + return; + } + + if (result.getData().get(0) instanceof RemoteFile remoteFile) { + String eTag = remoteFile.getEtag(); + mLocalFolder.setEtag(eTag); + + final FileDataStorageManager storageManager = getStorageManager(); + storageManager.saveFile(mLocalFolder); + } + } private void startDirectDownloads() { - FileDownloadHelper.Companion.instance().downloadFile(user, mLocalFolder); + final var fileDownloadHelper = FileDownloadHelper.Companion.instance(); + + if (syncInBackgroundWorker) { + try { + for (OCFile file: mFilesForDirectDownload) { + synchronized (mCancellationRequested) { + if (mCancellationRequested.get()) { + break; + } + } + + if (file == null) { + continue; + } + + final var operation = new DownloadFileOperation(user, file, mContext); + var result = operation.execute(getClient()); + + String filename = file.getFileName(); + if (filename == null) { + continue; + } + + if (result.isSuccess()) { + fileDownloadHelper.saveFile(file, operation, getStorageManager()); + Log_OC.d(TAG, "startDirectDownloads completed for: " + file.getFileName()); + } else { + Log_OC.d(TAG, "startDirectDownloads failed for: " + file.getFileName()); + } + } + } catch (Exception e) { + Log_OC.d(TAG, "Exception caught at startDirectDownloads" + e); + } + } else { + fileDownloadHelper.downloadFolder(mLocalFolder, user.getAccountName()); + } } /** diff --git a/app/src/main/java/com/owncloud/android/operations/UnshareOperation.java b/app/src/main/java/com/owncloud/android/operations/UnshareOperation.java index d45d8b6..3735703 100644 --- a/app/src/main/java/com/owncloud/android/operations/UnshareOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UnshareOperation.java @@ -5,7 +5,7 @@ * SPDX-FileCopyrightText: 2018 Andy Scherzinger * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2014 María Asensio Valverde - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations; @@ -113,6 +113,8 @@ public class UnshareOperation extends SyncOperation { RemoveShareRemoteOperation operation = new RemoveShareRemoteOperation(share.getRemoteId()); result = operation.execute(client); + boolean isFileExists = existsFile(client, file.getRemotePath()); + boolean isShareExists = getStorageManager().getShareById(shareId) != null; if (result.isSuccess()) { // E2E: unlock folder @@ -128,7 +130,7 @@ public class UnshareOperation extends SyncOperation { if (ShareType.PUBLIC_LINK == share.getShareType()) { file.setSharedViaLink(false); } else if (ShareType.USER == share.getShareType() || ShareType.GROUP == share.getShareType() - || ShareType.FEDERATED == share.getShareType()) { + || ShareType.FEDERATED == share.getShareType() || ShareType.FEDERATED_GROUP == share.getShareType()) { // Check if it is the last share List sharesWith = getStorageManager(). getSharesWithForAFile(remotePath, @@ -140,10 +142,12 @@ public class UnshareOperation extends SyncOperation { getStorageManager().saveFile(file); getStorageManager().removeShare(share); - - } else if (result.getCode() != ResultCode.MAINTENANCE_MODE && !existsFile(client, file.getRemotePath())) { - // unshare failed because file was deleted before + } else if (result.getCode() != ResultCode.MAINTENANCE_MODE && !isFileExists) { + // UnShare failed because file was deleted before getStorageManager().removeFile(file, true, true); + } else if (isShareExists && result.getCode() == ResultCode.FILE_NOT_FOUND) { + // UnShare failed because share was deleted before + getStorageManager().removeShare(share); } } else { diff --git a/app/src/main/java/com/owncloud/android/operations/UpdateNoteForShareOperation.java b/app/src/main/java/com/owncloud/android/operations/UpdateNoteForShareOperation.java index 972b7ec..d10b5e3 100644 --- a/app/src/main/java/com/owncloud/android/operations/UpdateNoteForShareOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UpdateNoteForShareOperation.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2018 Tobias Kaminsky * SPDX-FileCopyrightText: 2018 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.operations; diff --git a/app/src/main/java/com/owncloud/android/operations/UpdateOCVersionOperation.java b/app/src/main/java/com/owncloud/android/operations/UpdateOCVersionOperation.java index abd4ac1..e8f9fc1 100644 --- a/app/src/main/java/com/owncloud/android/operations/UpdateOCVersionOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UpdateOCVersionOperation.java @@ -6,7 +6,7 @@ * SPDX-FileCopyrightText: 2017 Andy Scherzinger * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2012 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations; diff --git a/app/src/main/java/com/owncloud/android/operations/UpdateShareInfoOperation.java b/app/src/main/java/com/owncloud/android/operations/UpdateShareInfoOperation.java index e69a426..3d21989 100644 --- a/app/src/main/java/com/owncloud/android/operations/UpdateShareInfoOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UpdateShareInfoOperation.java @@ -5,7 +5,7 @@ * Copyright (C) 2021 TSI-mc * Copyright (C) 2021 Nextcloud GmbH * - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.operations; @@ -16,6 +16,7 @@ import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.operations.RemoteOperation; import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.shares.GetShareRemoteOperation; import com.owncloud.android.lib.resources.shares.OCShare; import com.owncloud.android.lib.resources.shares.UpdateShareRemoteOperation; @@ -29,12 +30,16 @@ public class UpdateShareInfoOperation extends SyncOperation { private OCShare share; private long shareId; + private long shareRemoteId; private long expirationDateInMillis; private String note; private boolean hideFileDownload; private int permissions = -1; private String password; private String label; + private String attributes; + + private static final String TAG = "UpdateShareInfoOperation"; /** * Constructor @@ -58,9 +63,10 @@ public class UpdateShareInfoOperation extends SyncOperation { *

* this will be triggered while modifying existing share */ - public UpdateShareInfoOperation(long shareId, FileDataStorageManager storageManager) { + public UpdateShareInfoOperation(long shareId, long shareRemoteId, FileDataStorageManager storageManager) { super(storageManager); - + + this.shareRemoteId = shareRemoteId; this.shareId = shareId; expirationDateInMillis = 0L; note = null; @@ -76,9 +82,18 @@ public class UpdateShareInfoOperation extends SyncOperation { share = this.share; } + if (share == null && shareRemoteId > 0) { + Log_OC.w(TAG,"share is null, trying to fetch"); + final var shareRemoteOperation = new GetShareRemoteOperation(shareRemoteId); + final var result = shareRemoteOperation.execute(client); + if (result.isSuccess()) { + share = (OCShare) result.getData().get(0); + } + } + if (share == null) { - // TODO try to get remote share before failing? - return new RemoteOperationResult(RemoteOperationResult.ResultCode.SHARE_NOT_FOUND); + Log_OC.e(TAG,"share is null, fetching operation is failed"); + return new RemoteOperationResult<>(RemoteOperationResult.ResultCode.SHARE_NOT_FOUND); } // Update remote share @@ -93,11 +108,12 @@ public class UpdateShareInfoOperation extends SyncOperation { } updateOp.setPassword(password); updateOp.setLabel(label); + updateOp.setAttributes(attributes); - RemoteOperationResult result = updateOp.execute(client); + var result = updateOp.execute(client); if (result.isSuccess()) { - RemoteOperation getShareOp = new GetShareRemoteOperation(share.getRemoteId()); + final var getShareOp = new GetShareRemoteOperation(share.getRemoteId()); result = getShareOp.execute(client); //only update the share in storage if shareId is available @@ -105,9 +121,10 @@ public class UpdateShareInfoOperation extends SyncOperation { if (result.isSuccess() && shareId > 0) { OCShare ocShare = (OCShare) result.getData().get(0); ocShare.setPasswordProtected(!TextUtils.isEmpty(password)); + ocShare.setRemoteId(shareRemoteId); + ocShare.setId(shareId); getStorageManager().saveShare(ocShare); } - } return result; @@ -125,6 +142,10 @@ public class UpdateShareInfoOperation extends SyncOperation { this.hideFileDownload = hideFileDownload; } + public void setAttributes(String attributes) { + this.attributes = attributes; + } + public void setPermissions(int permissions) { this.permissions = permissions; } diff --git a/app/src/main/java/com/owncloud/android/operations/UpdateSharePermissionsOperation.java b/app/src/main/java/com/owncloud/android/operations/UpdateSharePermissionsOperation.java index 3e1eb53..aef7479 100644 --- a/app/src/main/java/com/owncloud/android/operations/UpdateSharePermissionsOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UpdateSharePermissionsOperation.java @@ -5,7 +5,7 @@ * SPDX-FileCopyrightText: 2018 Andy Scherzinger * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2015 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations; @@ -49,7 +49,7 @@ public class UpdateSharePermissionsOperation extends SyncOperation { @Override protected RemoteOperationResult run(OwnCloudClient client) { - OCShare share = getStorageManager().getShareById(shareId); // ShareType.USER | ShareType.GROUP + OCShare share = getStorageManager().getShareById(shareId); // ShareType.USER | ShareType.GROUP | ShareType.FEDERATED_GROUP if (share == null) { // TODO try to get remote share before failing? diff --git a/app/src/main/java/com/owncloud/android/operations/UpdateShareViaLinkOperation.java b/app/src/main/java/com/owncloud/android/operations/UpdateShareViaLinkOperation.java index a7c06ea..834c08a 100644 --- a/app/src/main/java/com/owncloud/android/operations/UpdateShareViaLinkOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UpdateShareViaLinkOperation.java @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2020-2021 Tobias Kaminsky * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2015 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations; diff --git a/app/src/main/java/com/owncloud/android/operations/UploadException.java b/app/src/main/java/com/owncloud/android/operations/UploadException.java index d29db71..080fb50 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadException.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadException.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2019 Andy Scherzinger - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.operations; diff --git a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java index 88101e6..ddeccdc 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -6,22 +6,23 @@ * SPDX-FileCopyrightText: 2017-2018 Andy Scherzinger * SPDX-FileCopyrightText: 2016 ownCloud Inc. * SPDX-FileCopyrightText: 2012 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations; import android.annotation.SuppressLint; import android.content.Context; -import android.content.Intent; import android.net.Uri; import android.text.TextUtils; import com.nextcloud.client.account.User; import com.nextcloud.client.device.BatteryStatus; import com.nextcloud.client.device.PowerManagementService; +import com.nextcloud.client.jobs.upload.FileUploadHelper; import com.nextcloud.client.jobs.upload.FileUploadWorker; import com.nextcloud.client.network.Connectivity; import com.nextcloud.client.network.ConnectivityService; +import com.nextcloud.utils.autoRename.AutoRename; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; import com.owncloud.android.datamodel.FileDataStorageManager; @@ -51,7 +52,13 @@ import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation; import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation; import com.owncloud.android.lib.resources.files.model.RemoteFile; import com.owncloud.android.lib.resources.status.E2EVersion; +import com.owncloud.android.lib.resources.status.OCCapability; import com.owncloud.android.operations.common.SyncOperation; +import com.owncloud.android.operations.e2e.E2EClientData; +import com.owncloud.android.operations.e2e.E2EData; +import com.owncloud.android.operations.e2e.E2EFiles; +import com.owncloud.android.operations.upload.UploadFileException; +import com.owncloud.android.operations.upload.UploadFileOperationExtensionsKt; import com.owncloud.android.utils.EncryptionUtils; import com.owncloud.android.utils.EncryptionUtilsV2; import com.owncloud.android.utils.FileStorageUtils; @@ -63,8 +70,6 @@ import com.owncloud.android.utils.theme.CapabilityUtils; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.methods.RequestEntity; -import org.lukhnos.nnio.file.Files; -import org.lukhnos.nnio.file.Paths; import java.io.File; import java.io.FileInputStream; @@ -77,20 +82,30 @@ import java.io.RandomAccessFile; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.channels.OverlappingFileLockException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.spec.InvalidParameterSpecException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import javax.crypto.BadPaddingException; import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; import androidx.annotation.CheckResult; import androidx.annotation.Nullable; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import static com.owncloud.android.ui.activity.FileDisplayActivity.REFRESH_FOLDER_EVENT_RECEIVER; +import kotlin.Triple; +import kotlin.Unit; /** * Operation performing the update in the ownCloud server of a file that was modified locally. @@ -102,6 +117,7 @@ public class UploadFileOperation extends SyncOperation { public static final int CREATED_BY_USER = 0; public static final int CREATED_AS_INSTANT_PICTURE = 1; public static final int CREATED_AS_INSTANT_VIDEO = 2; + public static final int MISSING_FILE_PERMISSION_NOTIFICATION_ID = 2501; /** * OCFile which is to be uploaded. @@ -149,6 +165,8 @@ public class UploadFileOperation extends SyncOperation { private final PowerManagementService powerManagementService; private boolean encryptedAncestor; + private OCFile duplicatedEncryptedFile; + private AtomicBoolean missingPermissionThrown = new AtomicBoolean(false); public static OCFile obtainNewOCFileToUpload(String remotePath, String localPath, String mimeType) { OCFile newFile = new OCFile(remotePath); @@ -217,25 +235,27 @@ public class UploadFileOperation extends SyncOperation { super(storageManager); if (upload == null) { + Log_OC.e(TAG, "UploadFileOperation upload is null cant construct"); throw new IllegalArgumentException("Illegal NULL file in UploadFileOperation creation"); } if (TextUtils.isEmpty(upload.getLocalPath())) { + Log_OC.e(TAG, "UploadFileOperation local path is null cant construct"); throw new IllegalArgumentException( "Illegal file in UploadFileOperation; storage path invalid: " + upload.getLocalPath()); } - + Log_OC.d(TAG, "creating upload file operation, user: " + user.getAccountName() + " upload account name " + upload.getAccountName()); this.uploadsStorageManager = uploadsStorageManager; this.connectivityService = connectivityService; this.powerManagementService = powerManagementService; this.user = user; mUpload = upload; if (file == null) { + Log_OC.w(TAG, "UploadFileOperation file is null, obtaining from upload"); mFile = obtainNewOCFileToUpload( upload.getRemotePath(), upload.getLocalPath(), - upload.getMimeType() - ); + upload.getMimeType()); } else { mFile = file; } @@ -386,40 +406,59 @@ public class UploadFileOperation extends SyncOperation { return mContext; } + public boolean isMissingPermissionThrown() { + return missingPermissionThrown.get(); + } + @Override @SuppressWarnings("PMD.AvoidDuplicateLiterals") protected RemoteOperationResult run(OwnCloudClient client) { + if (TextUtils.isEmpty(getStoragePath())) { + Log_OC.e(TAG, "Upload cancelled for " + getStoragePath() + ": file path is null or empty."); + return new RemoteOperationResult<>(new UploadFileException.EmptyOrNullFilePath()); + } + + final var localFile = new File(getStoragePath()); + if (!localFile.exists()) { + Log_OC.e(TAG, "Upload cancelled for " + getStoragePath() + ": local file not exists."); + return new RemoteOperationResult<>(ResultCode.LOCAL_FILE_NOT_FOUND); + } + + if (!localFile.canRead()) { + Log_OC.e(TAG, "Upload cancelled for " + getStoragePath() + ": file is not readable or inaccessible."); + UploadFileOperationExtensionsKt.showStoragePermissionNotification(this); + missingPermissionThrown.set(true); + return new RemoteOperationResult<>(new UploadFileException.MissingPermission()); + } + mCancellationRequested.set(false); mUploadStarted.set(true); updateSize(0); String remoteParentPath = new File(getRemotePath()).getParent(); - remoteParentPath = remoteParentPath.endsWith(OCFile.PATH_SEPARATOR) ? - remoteParentPath : remoteParentPath + OCFile.PATH_SEPARATOR; + remoteParentPath = remoteParentPath.endsWith(OCFile.PATH_SEPARATOR) ? remoteParentPath : remoteParentPath + OCFile.PATH_SEPARATOR; + remoteParentPath = AutoRename.INSTANCE.rename(remoteParentPath, getCapabilities()); OCFile parent = getStorageManager().getFileByPath(remoteParentPath); // in case of a fresh upload with subfolder, where parent does not exist yet if (parent == null && (mFolderUnlockToken == null || mFolderUnlockToken.isEmpty())) { // try to create folder - RemoteOperationResult result = grantFolderExistence(remoteParentPath, client); + final var result = grantFolderExistence(remoteParentPath, client); if (!result.isSuccess()) { return result; } parent = getStorageManager().getFileByPath(remoteParentPath); - - if (parent == null) { - return new RemoteOperationResult(false, "Parent folder not found", HttpStatus.SC_NOT_FOUND); - } } - // parent file is not null anymore: - // - it was created on fresh upload or - // - resume of encrypted upload, then parent file exists already as unlock is only for direct parent + if (parent == null) { + return new RemoteOperationResult<>(false, "Parent folder not found", HttpStatus.SC_NOT_FOUND); + } + // - resume of encrypted upload, then parent file exists already as unlock is only for direct parent mFile.setParentId(parent.getFileId()); // check if any parent is encrypted @@ -435,14 +474,11 @@ public class UploadFileOperation extends SyncOperation { } } - // TODO REFACTOR + // region E2E Upload @SuppressLint("AndroidLintUseSparseArrays") // gson cannot handle sparse arrays easily, therefore use hashmap private RemoteOperationResult encryptedUpload(OwnCloudClient client, OCFile parentFile) { RemoteOperationResult result = null; - File temporalFile = null; - File originalFile = new File(mOriginalStoragePath); - File expectedFile = null; - File encryptedTempFile = null; + E2EFiles e2eFiles = new E2EFiles(parentFile, null, new File(mOriginalStoragePath), null, null); FileLock fileLock = null; long size; @@ -454,29 +490,14 @@ public class UploadFileOperation extends SyncOperation { String publicKey = arbitraryDataProvider.getValue(user.getAccountName(), EncryptionUtils.PUBLIC_KEY); try { - // check conditions - result = checkConditions(originalFile); + result = checkConditions(e2eFiles.getOriginalFile()); if (result != null) { return result; } - /***** E2E *****/ - // Only on V2+: whenever we change something, increase counter - long counter = -1; - if (CapabilityUtils.getCapability(mContext).getEndToEndEncryptionApiVersion().compareTo(E2EVersion.V2_0) >= 0) { - counter = parentFile.getE2eCounter() + 1; - } - - // we might have an old token from interrupted upload - if (mFolderUnlockToken != null && !mFolderUnlockToken.isEmpty()) { - token = mFolderUnlockToken; - } else { - token = EncryptionUtils.lockFolder(parentFile, client, counter); - // immediately store it - mUpload.setFolderUnlockToken(token); - uploadsStorageManager.updateUpload(mUpload); - } + long counter = getE2ECounter(parentFile); + token = getFolderUnlockTokenOrLockFolder(client, parentFile, counter); // Update metadata EncryptionUtilsV2 encryptionUtilsV2 = new EncryptionUtilsV2(); @@ -485,301 +506,427 @@ public class UploadFileOperation extends SyncOperation { metadataExists = true; } - if (CapabilityUtils.getCapability(mContext).getEndToEndEncryptionApiVersion().compareTo(E2EVersion.V2_0) >= 0) { + if (isEndToEndVersionAtLeastV2()) { if (object == null) { - // TODO return error return new RemoteOperationResult(new IllegalStateException("Metadata does not exist")); } } else { - // v1 is allowed to be null, thus create it - DecryptedFolderMetadataFileV1 metadata = new DecryptedFolderMetadataFileV1(); - metadata.setMetadata(new DecryptedMetadata()); - metadata.getMetadata().setVersion(1.2); - metadata.getMetadata().setMetadataKeys(new HashMap<>()); - String metadataKey = EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.generateKey()); - String encryptedMetadataKey = EncryptionUtils.encryptStringAsymmetric(metadataKey, publicKey); - metadata.getMetadata().setMetadataKey(encryptedMetadataKey); - - if (object instanceof DecryptedFolderMetadataFileV1) { - metadata = (DecryptedFolderMetadataFileV1) object; - } - - object = metadata; + object = getDecryptedFolderMetadataV1(publicKey, object); } - // todo fail if no metadata + E2EClientData clientData = new E2EClientData(client, token, publicKey); -// metadataExists = metadataPair.getFirst(); -// DecryptedFolderMetadataFile metadata = metadataPair.getSecond(); + List fileNames = getCollidedFileNames(object); - // TODO E2E: check counter: must be less than our counter, check rest: signature, etc - /**** E2E *****/ - - // check name collision - List fileNames = new ArrayList<>(); - if (object instanceof DecryptedFolderMetadataFileV1 metadata) { - for (DecryptedFile file : metadata.getFiles().values()) { - fileNames.add(file.getEncrypted().getFilename()); - } - } else { - for (com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile file : - ((DecryptedFolderMetadataFile) object).getMetadata().getFiles().values()) { - fileNames.add(file.getFilename()); - } - } - - RemoteOperationResult collisionResult = checkNameCollision(client, fileNames, parentFile.isEncrypted()); + RemoteOperationResult collisionResult = checkNameCollision(parentFile, client, fileNames, parentFile.isEncrypted()); if (collisionResult != null) { result = collisionResult; return collisionResult; } - mFile.setDecryptedRemotePath(parentFile.getDecryptedRemotePath() + originalFile.getName()); + mFile.setDecryptedRemotePath(parentFile.getDecryptedRemotePath() + e2eFiles.getOriginalFile().getName()); String expectedPath = FileStorageUtils.getDefaultSavePathFor(user.getAccountName(), mFile); - expectedFile = new File(expectedPath); + e2eFiles.setExpectedFile(new File(expectedPath)); - result = copyFile(originalFile, expectedPath); + result = copyFile(e2eFiles.getOriginalFile(), expectedPath); if (!result.isSuccess()) { return result; } - // Get the last modification date of the file from the file system - long lastModifiedTimestamp = originalFile.lastModified() / 1000; - - Long creationTimestamp = FileUtil.getCreationTimestamp(originalFile); - - /***** E2E *****/ - byte[] key = EncryptionUtils.generateKey(); - byte[] iv = EncryptionUtils.randomBytes(EncryptionUtils.ivLength); - Cipher cipher = EncryptionUtils.getCipher(Cipher.ENCRYPT_MODE, key, iv); - File file = new File(mFile.getStoragePath()); - EncryptedFile encryptedFile = EncryptionUtils.encryptFile(user.getAccountName(), file, cipher); - - // new random file name, check if it exists in metadata - String encryptedFileName = EncryptionUtils.generateUid(); - - if (object instanceof DecryptedFolderMetadataFileV1 metadata) { - while (metadata.getFiles().get(encryptedFileName) != null) { - encryptedFileName = EncryptionUtils.generateUid(); - } - } else { - while (((DecryptedFolderMetadataFile) object).getMetadata().getFiles().get(encryptedFileName) != null) { - encryptedFileName = EncryptionUtils.generateUid(); - } + long lastModifiedTimestamp = e2eFiles.getOriginalFile().lastModified() / 1000; + Long creationTimestamp = FileUtil.getCreationTimestamp(e2eFiles.getOriginalFile()); + if (creationTimestamp == null) { + Log_OC.e(TAG, "UploadFileOperation creationTimestamp cannot be null"); + throw new NullPointerException("creationTimestamp cannot be null"); } - encryptedTempFile = encryptedFile.getEncryptedFile(); - - FileChannel channel = null; - try { - channel = new RandomAccessFile(mFile.getStoragePath(), "rw").getChannel(); - fileLock = channel.tryLock(); - } catch (FileNotFoundException e) { - // this basically means that the file is on SD card - // try to copy file to temporary dir if it doesn't exist - String temporalPath = FileStorageUtils.getInternalTemporalPath(user.getAccountName(), mContext) + - mFile.getRemotePath(); - mFile.setStoragePath(temporalPath); - temporalFile = new File(temporalPath); - - Files.deleteIfExists(Paths.get(temporalPath)); - result = copy(originalFile, temporalFile); - - if (result.isSuccess()) { - if (temporalFile.length() == originalFile.length()) { - channel = new RandomAccessFile(temporalFile.getAbsolutePath(), "rw").getChannel(); - fileLock = channel.tryLock(); - } else { - result = new RemoteOperationResult(ResultCode.LOCK_FAILED); - } - } + E2EData e2eData = getE2EData(object); + e2eFiles.setEncryptedTempFile(e2eData.getEncryptedFile().getEncryptedFile()); + if (e2eFiles.getEncryptedTempFile() == null) { + Log_OC.e(TAG, "UploadFileOperation encryptedTempFile cannot be null"); + throw new NullPointerException("encryptedTempFile cannot be null"); } - try { - size = channel.size(); - } catch (IOException e1) { - size = new File(mFile.getStoragePath()).length(); - } + Triple channelResult = initFileChannel(result, fileLock, e2eFiles); + fileLock = channelResult.getFirst(); + result = channelResult.getSecond(); + FileChannel channel = channelResult.getThird(); + size = getChannelSize(channel); updateSize(size); + setUploadOperationForE2E(token, e2eFiles.getEncryptedTempFile(), e2eData.getEncryptedFileName(), lastModifiedTimestamp, creationTimestamp, size); - /// perform the upload - if (size > ChunkedFileUploadRemoteOperation.CHUNK_SIZE_MOBILE) { - boolean onWifiConnection = connectivityService.getConnectivity().isWifi(); - - mUploadOperation = new ChunkedFileUploadRemoteOperation(encryptedTempFile.getAbsolutePath(), - mFile.getParentRemotePath() + encryptedFileName, - mFile.getMimeType(), - mFile.getEtagInConflict(), - lastModifiedTimestamp, - onWifiConnection, - token, - creationTimestamp, - mDisableRetries - ); - } else { - mUploadOperation = new UploadFileRemoteOperation(encryptedTempFile.getAbsolutePath(), - mFile.getParentRemotePath() + encryptedFileName, - mFile.getMimeType(), - mFile.getEtagInConflict(), - lastModifiedTimestamp, - creationTimestamp, - token, - mDisableRetries - ); - } - - for (OnDatatransferProgressListener mDataTransferListener : mDataTransferListeners) { - mUploadOperation.addDataTransferProgressListener(mDataTransferListener); - } - - if (mCancellationRequested.get()) { - throw new OperationCancelledException(); - } - - result = mUploadOperation.execute(client); - - /// move local temporal file or original file to its corresponding - // location in the Nextcloud local folder - if (!result.isSuccess() && result.getHttpCode() == HttpStatus.SC_PRECONDITION_FAILED) { - result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT); - } + result = performE2EUpload(clientData); if (result.isSuccess()) { - mFile.setDecryptedRemotePath(parentFile.getDecryptedRemotePath() + originalFile.getName()); - mFile.setRemotePath(parentFile.getRemotePath() + encryptedFileName); - - - if (object instanceof DecryptedFolderMetadataFileV1 metadata) { - // update metadata - DecryptedFile decryptedFile = new DecryptedFile(); - Data data = new Data(); - data.setFilename(mFile.getDecryptedFileName()); - data.setMimetype(mFile.getMimeType()); - data.setKey(EncryptionUtils.encodeBytesToBase64String(key)); - decryptedFile.setEncrypted(data); - decryptedFile.setInitializationVector(EncryptionUtils.encodeBytesToBase64String(iv)); - decryptedFile.setAuthenticationTag(encryptedFile.getAuthenticationTag()); - - metadata.getFiles().put(encryptedFileName, decryptedFile); - - EncryptedFolderMetadataFileV1 encryptedFolderMetadata = - EncryptionUtils.encryptFolderMetadata(metadata, - publicKey, - parentFile.getLocalId(), - user, - arbitraryDataProvider - ); - - String serializedFolderMetadata; - - // check if we need metadataKeys - if (metadata.getMetadata().getMetadataKey() != null) { - serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata, true); - } else { - serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata); - } - - // upload metadata - EncryptionUtils.uploadMetadata(parentFile, - serializedFolderMetadata, - token, - client, - metadataExists, - E2EVersion.V1_2, - "", - arbitraryDataProvider, - user); - } else { - DecryptedFolderMetadataFile metadata = (DecryptedFolderMetadataFile) object; - encryptionUtilsV2.addFileToMetadata( - encryptedFileName, - mFile, - iv, - encryptedFile.getAuthenticationTag(), - key, - metadata, - getStorageManager()); - - // upload metadata - encryptionUtilsV2.serializeAndUploadMetadata(parentFile, - metadata, - token, - client, - true, - mContext, - user, - getStorageManager()); - } + updateMetadataForE2E(object, e2eData, clientData, e2eFiles, arbitraryDataProvider, encryptionUtilsV2, metadataExists); } } catch (FileNotFoundException e) { - Log_OC.d(TAG, mFile.getStoragePath() + " does not exist anymore"); + Log_OC.e(TAG, mFile.getStoragePath() + " does not exist anymore"); result = new RemoteOperationResult(ResultCode.LOCAL_FILE_NOT_FOUND); } catch (OverlappingFileLockException e) { - Log_OC.d(TAG, "Overlapping file lock exception"); + Log_OC.e(TAG, "Overlapping file lock exception"); result = new RemoteOperationResult(ResultCode.LOCK_FAILED); } catch (Exception e) { + Log_OC.e(TAG, "UploadFileOperation exception: " + e.getLocalizedMessage()); result = new RemoteOperationResult(e); } finally { - mUploadStarted.set(false); - sendRefreshFolderEventBroadcast(); - - if (fileLock != null) { - try { - fileLock.release(); - } catch (IOException e) { - Log_OC.e(TAG, "Failed to unlock file with path " + mFile.getStoragePath()); - } - } - - if (temporalFile != null && !originalFile.equals(temporalFile)) { - temporalFile.delete(); - } - if (result == null) { - result = new RemoteOperationResult(ResultCode.UNKNOWN_ERROR); - } - - logResult(result, mFile.getStoragePath(), mFile.getRemotePath()); - - // Unlock must be done otherwise folder stays locked and user can't upload any file - RemoteOperationResult unlockFolderResult; - if (object instanceof DecryptedFolderMetadataFileV1) { - unlockFolderResult = EncryptionUtils.unlockFolderV1(parentFile, client, token); - } else { - unlockFolderResult = EncryptionUtils.unlockFolder(parentFile, client, token); - } - - if (unlockFolderResult != null && !unlockFolderResult.isSuccess()) { - result = unlockFolderResult; - } - - if (encryptedTempFile != null) { - boolean isTempEncryptedFileDeleted = encryptedTempFile.delete(); - Log_OC.e(TAG, "isTempEncryptedFileDeleted: " + isTempEncryptedFileDeleted); - } else { - Log_OC.e(TAG, "Encrypted temp file cannot be found"); - } + result = cleanupE2EUpload(fileLock, e2eFiles, result, object, client, token); } - if (result.isSuccess()) { - handleSuccessfulUpload(temporalFile, expectedFile, originalFile, client); - } else if (result.getCode() == ResultCode.SYNC_CONFLICT) { - getStorageManager().saveConflict(mFile, mFile.getEtagInConflict()); + completeE2EUpload(result, e2eFiles, client); + + return result; + } + + private boolean isEndToEndVersionAtLeastV2() { + return getE2EVersion().compareTo(E2EVersion.V2_0) >= 0; + } + + private E2EVersion getE2EVersion() { + return CapabilityUtils.getCapability(mContext).getEndToEndEncryptionApiVersion(); + } + + private long getE2ECounter(OCFile parentFile) { + long counter = -1; + + if (isEndToEndVersionAtLeastV2()) { + counter = parentFile.getE2eCounter() + 1; } - // delete temporal file - if (temporalFile != null && temporalFile.exists() && !temporalFile.delete()) { - Log_OC.e(TAG, "Could not delete temporal file " + temporalFile.getAbsolutePath()); + return counter; + } + + private String getFolderUnlockTokenOrLockFolder(OwnCloudClient client, OCFile parentFile, long counter) throws UploadException { + if (mFolderUnlockToken != null && !mFolderUnlockToken.isEmpty()) { + return mFolderUnlockToken; + } + + String token = EncryptionUtils.lockFolder(parentFile, client, counter); + mUpload.setFolderUnlockToken(token); + uploadsStorageManager.updateUpload(mUpload); + + return token; + } + + private DecryptedFolderMetadataFileV1 getDecryptedFolderMetadataV1(String publicKey, Object object) + throws NoSuchPaddingException, IllegalBlockSizeException, CertificateException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + + DecryptedFolderMetadataFileV1 metadata = new DecryptedFolderMetadataFileV1(); + metadata.setMetadata(new DecryptedMetadata()); + metadata.getMetadata().setVersion(1.2); + metadata.getMetadata().setMetadataKeys(new HashMap<>()); + String metadataKey = EncryptionUtils.encodeBytesToBase64String(EncryptionUtils.generateKey()); + String encryptedMetadataKey = EncryptionUtils.encryptStringAsymmetric(metadataKey, publicKey); + metadata.getMetadata().setMetadataKey(encryptedMetadataKey); + + if (object instanceof DecryptedFolderMetadataFileV1) { + metadata = (DecryptedFolderMetadataFileV1) object; + } + + return metadata; + } + + private List getCollidedFileNames(Object object) { + List result = new ArrayList<>(); + + if (object instanceof DecryptedFolderMetadataFileV1 metadata) { + for (DecryptedFile file : metadata.getFiles().values()) { + result.add(file.getEncrypted().getFilename()); + } + } else if (object instanceof DecryptedFolderMetadataFile metadataFile) { + Map files = metadataFile.getMetadata().getFiles(); + for (com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFile file : files.values()) { + result.add(file.getFilename()); + } } return result; } - private void sendRefreshFolderEventBroadcast() { - Intent intent = new Intent(REFRESH_FOLDER_EVENT_RECEIVER); - LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent); + private String getEncryptedFileName(Object object) { + String encryptedFileName = EncryptionUtils.generateUid(); + + if (object instanceof DecryptedFolderMetadataFileV1 metadata) { + while (metadata.getFiles().get(encryptedFileName) != null) { + encryptedFileName = EncryptionUtils.generateUid(); + } + } else { + while (((DecryptedFolderMetadataFile) object).getMetadata().getFiles().get(encryptedFileName) != null) { + encryptedFileName = EncryptionUtils.generateUid(); + } + } + + return encryptedFileName; } + private void setUploadOperationForE2E(String token, + File encryptedTempFile, + String encryptedFileName, + long lastModifiedTimestamp, + long creationTimestamp, + long size) { + + if (size > ChunkedFileUploadRemoteOperation.CHUNK_SIZE_MOBILE) { + boolean onWifiConnection = connectivityService.getConnectivity().isWifi(); + + mUploadOperation = new ChunkedFileUploadRemoteOperation(encryptedTempFile.getAbsolutePath(), + mFile.getParentRemotePath() + encryptedFileName, + mFile.getMimeType(), + mFile.getEtagInConflict(), + lastModifiedTimestamp, + onWifiConnection, + token, + creationTimestamp, + mDisableRetries + ); + } else { + mUploadOperation = new UploadFileRemoteOperation(encryptedTempFile.getAbsolutePath(), + mFile.getParentRemotePath() + encryptedFileName, + mFile.getMimeType(), + mFile.getEtagInConflict(), + lastModifiedTimestamp, + creationTimestamp, + token, + mDisableRetries + ); + } + } + + private Triple initFileChannel(RemoteOperationResult result, FileLock fileLock, E2EFiles e2eFiles) throws IOException { + FileChannel channel = null; + + try (RandomAccessFile randomAccessFile = new RandomAccessFile(mFile.getStoragePath(), "rw")) { + channel = randomAccessFile.getChannel(); + fileLock = channel.tryLock(); + } catch (IOException ioException) { + Log_OC.d(TAG, "Error caught at getChannelFromFile: " + ioException); + + // this basically means that the file is on SD card + // try to copy file to temporary dir if it doesn't exist + String temporalPath = FileStorageUtils.getInternalTemporalPath(user.getAccountName(), mContext) + + mFile.getRemotePath(); + mFile.setStoragePath(temporalPath); + e2eFiles.setTemporalFile(new File(temporalPath)); + + if (e2eFiles.getTemporalFile() == null) { + throw new NullPointerException("Original file cannot be null"); + } + + Files.deleteIfExists(Paths.get(temporalPath)); + result = copy(e2eFiles.getOriginalFile(), e2eFiles.getTemporalFile()); + + if (result.isSuccess()) { + if (e2eFiles.getTemporalFile().length() == e2eFiles.getOriginalFile().length()) { + try (RandomAccessFile randomAccessFile = new RandomAccessFile(e2eFiles.getTemporalFile().getAbsolutePath(), "rw")) { + channel = randomAccessFile.getChannel(); + fileLock = channel.tryLock(); + } catch (IOException e) { + Log_OC.d(TAG, "Error caught at getChannelFromFile: " + e); + } + } else { + result = new RemoteOperationResult(ResultCode.LOCK_FAILED); + } + } + } + + return new Triple<>(fileLock, result, channel); + } + + private long getChannelSize(FileChannel channel) { + try { + return channel.size(); + } catch (IOException e1) { + return new File(mFile.getStoragePath()).length(); + } + } + + private RemoteOperationResult performE2EUpload(E2EClientData data) throws OperationCancelledException { + for (OnDatatransferProgressListener mDataTransferListener : mDataTransferListeners) { + mUploadOperation.addDataTransferProgressListener(mDataTransferListener); + } + + if (mCancellationRequested.get()) { + throw new OperationCancelledException(); + } + + RemoteOperationResult result = mUploadOperation.execute(data.getClient()); + + /// move local temporal file or original file to its corresponding + // location in the Nextcloud local folder + if (!result.isSuccess() && result.getHttpCode() == HttpStatus.SC_PRECONDITION_FAILED) { + result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT); + } + + return result; + } + + private E2EData getE2EData(Object object) throws InvalidAlgorithmParameterException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, InvalidParameterSpecException, IOException { + byte[] key = EncryptionUtils.generateKey(); + byte[] iv = EncryptionUtils.randomBytes(EncryptionUtils.ivLength); + Cipher cipher = EncryptionUtils.getCipher(Cipher.ENCRYPT_MODE, key, iv); + File file = new File(mFile.getStoragePath()); + EncryptedFile encryptedFile = EncryptionUtils.encryptFile(user.getAccountName(), file, cipher); + String encryptedFileName = getEncryptedFileName(object); + + if (key == null) { + throw new NullPointerException("key cannot be null"); + } + + return new E2EData(key, iv, encryptedFile, encryptedFileName); + } + + private void updateMetadataForE2E(Object object, E2EData e2eData, E2EClientData clientData, E2EFiles e2eFiles, ArbitraryDataProvider arbitraryDataProvider, EncryptionUtilsV2 encryptionUtilsV2, boolean metadataExists) + + throws InvalidAlgorithmParameterException, UploadException, NoSuchPaddingException, IllegalBlockSizeException, CertificateException, + NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + + final var filename = new File(mFile.getRemotePath()).getName(); + mFile.setDecryptedRemotePath(e2eFiles.getParentFile().getDecryptedRemotePath() + filename); + mFile.setRemotePath(e2eFiles.getParentFile().getRemotePath() + e2eData.getEncryptedFileName()); + + + if (object instanceof DecryptedFolderMetadataFileV1 metadata) { + updateMetadataForV1(metadata, + e2eData, + clientData, + e2eFiles.getParentFile(), + arbitraryDataProvider, + metadataExists); + } else if (object instanceof DecryptedFolderMetadataFile metadata) { + updateMetadataForV2(metadata, + encryptionUtilsV2, + e2eData, + clientData, + e2eFiles.getParentFile()); + } + } + + private void updateMetadataForV1(DecryptedFolderMetadataFileV1 metadata, E2EData e2eData, E2EClientData clientData, + OCFile parentFile, ArbitraryDataProvider arbitraryDataProvider, boolean metadataExists) + + throws InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, + CertificateException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException, UploadException { + + DecryptedFile decryptedFile = new DecryptedFile(); + Data data = new Data(); + data.setFilename(mFile.getDecryptedFileName()); + data.setMimetype(mFile.getMimeType()); + data.setKey(EncryptionUtils.encodeBytesToBase64String(e2eData.getKey())); + decryptedFile.setEncrypted(data); + decryptedFile.setInitializationVector(EncryptionUtils.encodeBytesToBase64String(e2eData.getIv())); + decryptedFile.setAuthenticationTag(e2eData.getEncryptedFile().getAuthenticationTag()); + + metadata.getFiles().put(e2eData.getEncryptedFileName(), decryptedFile); + + EncryptedFolderMetadataFileV1 encryptedFolderMetadata = + EncryptionUtils.encryptFolderMetadata(metadata, + clientData.getPublicKey(), + parentFile.getLocalId(), + user, + arbitraryDataProvider + ); + + String serializedFolderMetadata; + + if (metadata.getMetadata().getMetadataKey() != null) { + serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata, true); + } else { + serializedFolderMetadata = EncryptionUtils.serializeJSON(encryptedFolderMetadata); + } + + // upload metadata + EncryptionUtils.uploadMetadata(parentFile, + serializedFolderMetadata, + clientData.getToken(), + clientData.getClient(), + metadataExists, + E2EVersion.V1_2, + "", + arbitraryDataProvider, + user); + } + + + private void updateMetadataForV2(DecryptedFolderMetadataFile metadata, EncryptionUtilsV2 encryptionUtilsV2, E2EData e2eData, E2EClientData clientData, OCFile parentFile) throws UploadException { + encryptionUtilsV2.addFileToMetadata( + e2eData.getEncryptedFileName(), + mFile, + e2eData.getIv(), + e2eData.getEncryptedFile().getAuthenticationTag(), + e2eData.getKey(), + metadata, + getStorageManager()); + + // upload metadata + encryptionUtilsV2.serializeAndUploadMetadata(parentFile, + metadata, + clientData.getToken(), + clientData.getClient(), + true, + mContext, + user, + getStorageManager()); + } + + private void completeE2EUpload(RemoteOperationResult result, E2EFiles e2eFiles, OwnCloudClient client) { + if (result.isSuccess()) { + handleLocalBehaviour(e2eFiles.getTemporalFile(), e2eFiles.getExpectedFile(), e2eFiles.getOriginalFile(), client); + } else if (result.getCode() == ResultCode.SYNC_CONFLICT) { + getStorageManager().saveConflict(mFile, mFile.getEtagInConflict()); + } + + e2eFiles.deleteTemporalFile(); + } + + private RemoteOperationResult cleanupE2EUpload(FileLock fileLock, E2EFiles e2eFiles, RemoteOperationResult result, Object object, OwnCloudClient client, String token) { + mUploadStarted.set(false); + + if (fileLock != null) { + try { + fileLock.release(); + } catch (IOException e) { + Log_OC.e(TAG, "Failed to unlock file with path " + mFile.getStoragePath()); + } + } + + e2eFiles.deleteTemporalFileWithOriginalFileComparison(); + + if (result == null) { + result = new RemoteOperationResult(ResultCode.UNKNOWN_ERROR); + } + + logResult(result, mFile.getStoragePath(), mFile.getRemotePath()); + + // Unlock must be done otherwise folder stays locked and user can't upload any file + RemoteOperationResult unlockFolderResult; + if (object instanceof DecryptedFolderMetadataFileV1) { + unlockFolderResult = EncryptionUtils.unlockFolderV1(e2eFiles.getParentFile(), client, token); + } else { + unlockFolderResult = EncryptionUtils.unlockFolder(e2eFiles.getParentFile(), client, token); + } + + if (unlockFolderResult != null && !unlockFolderResult.isSuccess()) { + result = unlockFolderResult; + } + + if (unlockFolderResult != null && unlockFolderResult.isSuccess()) { + Log_OC.d(TAG, "Folder successfully unlocked: " + e2eFiles.getParentFile().getFileName()); + + if (duplicatedEncryptedFile != null) { + FileUploadHelper.Companion.instance().removeDuplicatedFile(duplicatedEncryptedFile, client, user, () -> { + duplicatedEncryptedFile = null; + return Unit.INSTANCE; + }); + } + } + + e2eFiles.deleteEncryptedTempFile(); + + return result; + } + // endregion + private RemoteOperationResult checkConditions(File originalFile) { RemoteOperationResult remoteOperationResult = null; @@ -836,7 +983,7 @@ public class UploadFileOperation extends SyncOperation { } // check name collision - RemoteOperationResult collisionResult = checkNameCollision(client, null, false); + final var collisionResult = checkNameCollision(null, client, null, false); if (collisionResult != null) { result = collisionResult; return collisionResult; @@ -874,14 +1021,15 @@ public class UploadFileOperation extends SyncOperation { channel = new RandomAccessFile(temporalFile.getAbsolutePath(), "rw").getChannel(); fileLock = channel.tryLock(); } else { - result = new RemoteOperationResult(ResultCode.LOCK_FAILED); + result = new RemoteOperationResult<>(ResultCode.LOCK_FAILED); } } } try { size = channel.size(); - } catch (Exception e1) { + } catch (Exception exception) { + Log_OC.e(TAG, "normalUpload, size cannot be determined from channel: " + exception); size = new File(mFile.getStoragePath()).length(); } @@ -909,6 +1057,10 @@ public class UploadFileOperation extends SyncOperation { mDisableRetries); } + /** + * Adds the onTransferProgress in FileUploadWorker + * {@link FileUploadWorker#onTransferProgress(long, long, long, String)()} + */ for (OnDatatransferProgressListener mDataTransferListener : mDataTransferListeners) { mUploadOperation.addDataTransferProgressListener(mDataTransferListener); } @@ -923,17 +1075,17 @@ public class UploadFileOperation extends SyncOperation { /// move local temporal file or original file to its corresponding // location in the Nextcloud local folder if (!result.isSuccess() && result.getHttpCode() == HttpStatus.SC_PRECONDITION_FAILED) { - result = new RemoteOperationResult(ResultCode.SYNC_CONFLICT); + result = new RemoteOperationResult<>(ResultCode.SYNC_CONFLICT); } } } catch (FileNotFoundException e) { Log_OC.d(TAG, mOriginalStoragePath + " not exists anymore"); - result = new RemoteOperationResult(ResultCode.LOCAL_FILE_NOT_FOUND); + result = new RemoteOperationResult<>(ResultCode.LOCAL_FILE_NOT_FOUND); } catch (OverlappingFileLockException e) { Log_OC.d(TAG, "Overlapping file lock exception"); - result = new RemoteOperationResult(ResultCode.LOCK_FAILED); + result = new RemoteOperationResult<>(ResultCode.LOCK_FAILED); } catch (Exception e) { - result = new RemoteOperationResult(e); + result = new RemoteOperationResult<>(e); } finally { mUploadStarted.set(false); @@ -954,18 +1106,19 @@ public class UploadFileOperation extends SyncOperation { } if (temporalFile != null && !originalFile.equals(temporalFile)) { - temporalFile.delete(); + boolean isTempFileDeleted = temporalFile.delete(); + Log_OC.d(TAG, "normalUpload, temp folder deletion: " + isTempFileDeleted); } if (result == null) { - result = new RemoteOperationResult(ResultCode.UNKNOWN_ERROR); + result = new RemoteOperationResult<>(ResultCode.UNKNOWN_ERROR); } logResult(result, mOriginalStoragePath, mRemotePath); } if (result.isSuccess()) { - handleSuccessfulUpload(temporalFile, expectedFile, originalFile, client); + handleLocalBehaviour(temporalFile, expectedFile, originalFile, client); } else if (result.getCode() == ResultCode.SYNC_CONFLICT) { getStorageManager().saveConflict(mFile, mFile.getEtagInConflict()); } @@ -1023,17 +1176,20 @@ public class UploadFileOperation extends SyncOperation { } @CheckResult - private RemoteOperationResult checkNameCollision(OwnCloudClient client, + private RemoteOperationResult checkNameCollision(OCFile parentFile, + OwnCloudClient client, List fileNames, boolean encrypted) throws OperationCancelledException { Log_OC.d(TAG, "Checking name collision in server"); - if (existsFile(client, mRemotePath, fileNames, encrypted)) { + boolean isFileExists = existsFile(client, mRemotePath, fileNames, encrypted); + + if (isFileExists) { switch (mNameCollisionPolicy) { - case CANCEL: - Log_OC.d(TAG, "File exists; canceling"); - throw new OperationCancelledException(); + case SKIP: + Log_OC.d(TAG, "user choose to skip upload if same file exists"); + return new RemoteOperationResult<>(ResultCode.OK); case RENAME: mRemotePath = getNewAvailableRemotePath(client, mRemotePath, fileNames, encrypted); mWasRenamed = true; @@ -1044,6 +1200,10 @@ public class UploadFileOperation extends SyncOperation { } break; case OVERWRITE: + if (parentFile != null && encrypted) { + duplicatedEncryptedFile = getStorageManager().findDuplicatedFile(parentFile, mFile); + } + Log_OC.d(TAG, "Overwriting file"); break; case ASK_USER: @@ -1059,19 +1219,38 @@ public class UploadFileOperation extends SyncOperation { return null; } - private void handleSuccessfulUpload(File temporalFile, - File expectedFile, - File originalFile, - OwnCloudClient client) { - switch (mLocalBehaviour) { - case FileUploadWorker.LOCAL_BEHAVIOUR_FORGET: - default: - mFile.setStoragePath(""); - saveUploadedFile(client); - break; + public void handleLocalBehaviour() { + if (user == null || mFile == null || mContext == null) { + Log_OC.d(TAG, "handleLocalBehaviour: user, file, or context is null."); + return; + } + final var client = getClient(); + if (client == null) { + Log_OC.d(TAG, "handleLocalBehaviour: client is null"); + return; + } + + String expectedPath = FileStorageUtils.getDefaultSavePathFor(user.getAccountName(), mFile); + File expectedFile = new File(expectedPath); + File originalFile = new File(mOriginalStoragePath); + String temporalPath = FileStorageUtils.getInternalTemporalPath(user.getAccountName(), mContext) + mFile.getRemotePath(); + File temporalFile = new File(temporalPath); + + handleLocalBehaviour(temporalFile, expectedFile, originalFile, client); + } + + private void handleLocalBehaviour(File temporalFile, + File expectedFile, + File originalFile, + OwnCloudClient client) { + switch (mLocalBehaviour) { case FileUploadWorker.LOCAL_BEHAVIOUR_DELETE: - originalFile.delete(); + try { + Files.delete(originalFile.toPath()); + } catch (IOException e) { + Log_OC.e(TAG, "Could not delete original file: " + originalFile.getAbsolutePath(), e); + } mFile.setStoragePath(""); getStorageManager().deleteFileInMediaScan(originalFile.getAbsolutePath()); saveUploadedFile(client); @@ -1114,9 +1293,18 @@ public class UploadFileOperation extends SyncOperation { FileDataStorageManager.triggerMediaScan(newFile.getAbsolutePath()); } break; + + default: + mFile.setStoragePath(""); + saveUploadedFile(client); + break; } } + private OCCapability getCapabilities() { + return CapabilityUtils.getCapability(mContext); + } + /** * Checks the existence of the folder where the current file will be uploaded both in the remote server and in the * local database. @@ -1200,7 +1388,7 @@ public class UploadFileOperation extends SyncOperation { * @param fileNames list of decrypted file names * @return new remote path */ - private String getNewAvailableRemotePath(OwnCloudClient client, + public static String getNewAvailableRemotePath(OwnCloudClient client, String remotePath, List fileNames, boolean encrypted) { @@ -1226,7 +1414,7 @@ public class UploadFileOperation extends SyncOperation { return newPath; } - private boolean existsFile(OwnCloudClient client, + private static boolean existsFile(OwnCloudClient client, String remotePath, List fileNames, boolean encrypted) { @@ -1248,21 +1436,37 @@ public class UploadFileOperation extends SyncOperation { } /** - * Allows to cancel the actual upload operation. If actual upload operating is in progress it is cancelled, if - * upload preparation is being performed upload will not take place. + * Cancels the current upload process. + * + *

+ * Behavior depends on the current state of the upload: + *

    + *
  • Upload in preparation: Upload will not start and a cancellation flag is set.
  • + *
  • Upload in progress: The ongoing upload operation is cancelled via + * {@link UploadFileRemoteOperation#cancel(ResultCode)}.
  • + *
  • No upload operation: A cancellation flag is still set, but this situation is unexpected + * and logged as an error.
  • + *
+ * + *

+ * Once cancelled, the database will be updated through + * {@link UploadsStorageManager#updateDatabaseUploadResult(RemoteOperationResult, UploadFileOperation)}. + * + * @param cancellationReason the reason for cancellation */ public void cancel(ResultCode cancellationReason) { - if (mUploadOperation == null) { - if (mUploadStarted.get()) { - Log_OC.d(TAG, "Cancelling upload during upload preparations."); - mCancellationRequested.set(true); - } else { - mCancellationRequested.set(true); - Log_OC.e(TAG, "No upload in progress. This should not happen."); - } - } else { + if (mUploadOperation != null) { + // Cancel an active upload Log_OC.d(TAG, "Cancelling upload during actual upload operation."); mUploadOperation.cancel(cancellationReason); + } else { + // Cancel while preparing or when no upload exists + mCancellationRequested.set(true); + if (mUploadStarted.get()) { + Log_OC.d(TAG, "Cancelling upload during preparation."); + } else { + Log_OC.e(TAG, "No upload in progress. This should not happen."); + } } } @@ -1366,28 +1570,22 @@ public class UploadFileOperation extends SyncOperation { if (!targetFile.equals(sourceFile)) { File expectedFolder = targetFile.getParentFile(); - expectedFolder.mkdirs(); + Files.createDirectories(expectedFolder.toPath()); if (expectedFolder.isDirectory()) { if (!sourceFile.renameTo(targetFile)) { // try to copy and then delete - targetFile.createNewFile(); - FileChannel inChannel = new FileInputStream(sourceFile).getChannel(); - FileChannel outChannel = new FileOutputStream(targetFile).getChannel(); - try { + Files.createFile(targetFile.toPath()); + try ( + FileChannel inChannel = new FileInputStream(sourceFile).getChannel(); + FileChannel outChannel = new FileOutputStream(targetFile).getChannel() + ) { inChannel.transferTo(0, inChannel.size(), outChannel); - sourceFile.delete(); + Files.delete(sourceFile.toPath()); } catch (Exception e) { mFile.setStoragePath(""); // forget the local file // by now, treat this as a success; the file was uploaded // the best option could be show a warning message - } finally { - if (inChannel != null) { - inChannel.close(); - } - if (outChannel != null) { - outChannel.close(); - } } } @@ -1469,11 +1667,11 @@ public class UploadFileOperation extends SyncOperation { file.setEtag(remoteFile.getEtag()); file.setRemoteId(remoteFile.getRemoteId()); file.setPermissions(remoteFile.getPermissions()); + file.setUploadTimestamp(remoteFile.getUploadTimestamp()); } public interface OnRenameListener { void onRenameUpload(); } - } diff --git a/app/src/main/java/com/owncloud/android/operations/common/SyncOperation.java b/app/src/main/java/com/owncloud/android/operations/common/SyncOperation.java index ea71c44..27dfbd1 100644 --- a/app/src/main/java/com/owncloud/android/operations/common/SyncOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/common/SyncOperation.java @@ -5,7 +5,7 @@ * SPDX-FileCopyrightText: 2021 Tobias Kaminsky * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2014 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.operations.common; @@ -46,8 +46,7 @@ public abstract class SyncOperation extends RemoteOperation { */ public RemoteOperationResult execute(Context context) { if (storageManager.getUser().isAnonymous()) { - throw new IllegalArgumentException("Trying to execute a sync operation with a " + - "storage manager for an anonymous account"); + return new RemoteOperationResult(RemoteOperationResult.ResultCode.ACCOUNT_EXCEPTION); } return super.execute(this.storageManager.getUser(), context); } diff --git a/app/src/main/java/com/owncloud/android/operations/e2e/E2EClientData.kt b/app/src/main/java/com/owncloud/android/operations/e2e/E2EClientData.kt new file mode 100644 index 0000000..892152a --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/e2e/E2EClientData.kt @@ -0,0 +1,12 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.operations.e2e + +import com.owncloud.android.lib.common.OwnCloudClient + +data class E2EClientData(val client: OwnCloudClient, val token: String, val publicKey: String) diff --git a/app/src/main/java/com/owncloud/android/operations/e2e/E2EData.kt b/app/src/main/java/com/owncloud/android/operations/e2e/E2EData.kt new file mode 100644 index 0000000..003d216 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/e2e/E2EData.kt @@ -0,0 +1,17 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.operations.e2e + +import com.owncloud.android.datamodel.e2e.v1.encrypted.EncryptedFile + +data class E2EData( + val key: ByteArray, + val iv: ByteArray, + val encryptedFile: EncryptedFile, + val encryptedFileName: String +) diff --git a/app/src/main/java/com/owncloud/android/operations/e2e/E2EFiles.kt b/app/src/main/java/com/owncloud/android/operations/e2e/E2EFiles.kt new file mode 100644 index 0000000..d67a095 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/e2e/E2EFiles.kt @@ -0,0 +1,46 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.operations.e2e + +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC +import java.io.File + +data class E2EFiles( + var parentFile: OCFile, + var temporalFile: File?, + var originalFile: File, + var expectedFile: File?, + var encryptedTempFile: File? +) { + private val tag = "E2EFiles" + + fun deleteTemporalFile() { + if (temporalFile?.exists() == true && temporalFile?.delete() == false) { + Log_OC.e(tag, "Could not delete temporal file " + temporalFile?.absolutePath) + } + } + + fun deleteTemporalFileWithOriginalFileComparison() { + if (originalFile == temporalFile) { + return + } + + val isTemporalFileDeleted = temporalFile?.delete() + Log_OC.d(tag, "isTemporalFileDeleted: $isTemporalFileDeleted") + } + + fun deleteEncryptedTempFile() { + if (encryptedTempFile != null) { + val isTempEncryptedFileDeleted = encryptedTempFile?.delete() + Log_OC.e(tag, "isTempEncryptedFileDeleted: $isTempEncryptedFileDeleted") + } else { + Log_OC.e(tag, "Encrypted temp file cannot be found") + } + } +} diff --git a/app/src/main/java/com/owncloud/android/operations/upload/UploadFileBroadcastReceiver.kt b/app/src/main/java/com/owncloud/android/operations/upload/UploadFileBroadcastReceiver.kt new file mode 100644 index 0000000..2d35262 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/upload/UploadFileBroadcastReceiver.kt @@ -0,0 +1,61 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.operations.upload + +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.Settings +import androidx.annotation.RequiresApi +import androidx.core.content.IntentCompat +import androidx.core.net.toUri +import com.owncloud.android.operations.UploadFileOperation + +class UploadFileBroadcastReceiver : BroadcastReceiver() { + companion object { + const val ACTION_TYPE = "UploadFileBroadcastReceiver.ACTION_TYPE" + } + + override fun onReceive(context: Context, intent: Intent) { + val actionType = + IntentCompat.getSerializableExtra(intent, ACTION_TYPE, UploadFileBroadcastReceiverActions::class.java) + ?: return + + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(UploadFileOperation.MISSING_FILE_PERMISSION_NOTIFICATION_ID) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && + actionType == UploadFileBroadcastReceiverActions.ALLOW_ALL_FILES + ) { + redirectToAllFilesAccess(context) + } else { + redirectToAppInfo(context) + } + } + + @RequiresApi(Build.VERSION_CODES.R) + private fun redirectToAllFilesAccess(context: Context) { + Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply { + data = "package:${context.packageName}".toUri() + flags = Intent.FLAG_ACTIVITY_NEW_TASK + }.run { + context.startActivity(this) + } + } + + private fun redirectToAppInfo(context: Context) { + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", context.packageName, null) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + }.run { + context.startActivity(this) + } + } +} diff --git a/app/src/main/java/com/owncloud/android/operations/upload/UploadFileBroadcastReceiverActions.kt b/app/src/main/java/com/owncloud/android/operations/upload/UploadFileBroadcastReceiverActions.kt new file mode 100644 index 0000000..c0d00d4 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/upload/UploadFileBroadcastReceiverActions.kt @@ -0,0 +1,13 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.operations.upload + +enum class UploadFileBroadcastReceiverActions : java.io.Serializable { + ALLOW_ALL_FILES, + APP_PERMISSIONS +} diff --git a/app/src/main/java/com/owncloud/android/operations/upload/UploadFileException.kt b/app/src/main/java/com/owncloud/android/operations/upload/UploadFileException.kt new file mode 100644 index 0000000..880a5eb --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/upload/UploadFileException.kt @@ -0,0 +1,13 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.operations.upload + +sealed class UploadFileException(message: String) : Exception(message) { + class EmptyOrNullFilePath : UploadFileException("Empty or null file path") + class MissingPermission : UploadFileException("Missing storage permission") +} diff --git a/app/src/main/java/com/owncloud/android/operations/upload/UploadFileOperationExtensions.kt b/app/src/main/java/com/owncloud/android/operations/upload/UploadFileOperationExtensions.kt new file mode 100644 index 0000000..72ba621 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/upload/UploadFileOperationExtensions.kt @@ -0,0 +1,76 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.operations.upload + +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import com.owncloud.android.R +import com.owncloud.android.operations.UploadFileOperation +import com.owncloud.android.operations.UploadFileOperation.MISSING_FILE_PERMISSION_NOTIFICATION_ID +import com.owncloud.android.ui.notifications.NotificationUtils + +fun UploadFileOperation.showStoragePermissionNotification() { + val notificationManager = ContextCompat.getSystemService(context, NotificationManager::class.java) + ?: return + val alreadyShown = notificationManager.activeNotifications.any { + it.id == MISSING_FILE_PERMISSION_NOTIFICATION_ID + } + if (alreadyShown) { + return + } + + val allowAllFileAccessAction = getAllowAllFileAccessAction(context) + val appPermissionsAction = getAppPermissionsAction(context) + + val notificationBuilder = + NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD) + .setSmallIcon(android.R.drawable.stat_sys_warning) + .setContentTitle(context.getString(R.string.upload_missing_storage_permission_title)) + .setContentText(context.getString(R.string.upload_missing_storage_permission_description)) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .addAction(allowAllFileAccessAction) + .addAction(appPermissionsAction) + .setAutoCancel(true) + + notificationManager.notify(MISSING_FILE_PERMISSION_NOTIFICATION_ID, notificationBuilder.build()) +} + +private fun getActionPendingIntent(context: Context, actionType: UploadFileBroadcastReceiverActions): PendingIntent { + val intent = Intent(context, UploadFileBroadcastReceiver::class.java).apply { + action = "com.owncloud.android.ACTION_UPLOAD_FILE_PERMISSION" + putExtra(UploadFileBroadcastReceiver.ACTION_TYPE, actionType) + } + + return PendingIntent.getBroadcast( + context, + actionType.ordinal, + intent, + PendingIntent.FLAG_IMMUTABLE + ) +} + +private fun getAllowAllFileAccessAction(context: Context): NotificationCompat.Action { + val pendingIntent = getActionPendingIntent(context, UploadFileBroadcastReceiverActions.ALLOW_ALL_FILES) + return NotificationCompat.Action( + null, + context.getString(R.string.upload_missing_storage_permission_allow_file_access), + pendingIntent + ) +} + +private fun getAppPermissionsAction(context: Context): NotificationCompat.Action { + val pendingIntent = getActionPendingIntent(context, UploadFileBroadcastReceiverActions.APP_PERMISSIONS) + return NotificationCompat.Action( + null, + context.getString(R.string.upload_missing_storage_permission_app_permissions), + pendingIntent + ) +} diff --git a/app/src/main/java/com/owncloud/android/providers/DiskLruImageCacheFileProvider.java b/app/src/main/java/com/owncloud/android/providers/DiskLruImageCacheFileProvider.java index 9053562..2ec6a71 100644 --- a/app/src/main/java/com/owncloud/android/providers/DiskLruImageCacheFileProvider.java +++ b/app/src/main/java/com/owncloud/android/providers/DiskLruImageCacheFileProvider.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2017 Tobias Kaminsky * SPDX-FileCopyrightText: 2017 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.providers; @@ -29,6 +29,7 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; +import java.nio.file.Files; import javax.inject.Inject; @@ -78,7 +79,7 @@ public class DiskLruImageCacheFileProvider extends ContentProvider { // create a file to write bitmap data File f = new File(MainApp.getAppContext().getCacheDir(), ocFile.getFileName()); try { - f.createNewFile(); + Files.createFile(f.toPath()); //Convert bitmap to byte array ByteArrayOutputStream bos = new ByteArrayOutputStream(); diff --git a/app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java b/app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java index cc8b20a..3ad48e4 100644 --- a/app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java +++ b/app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2020-2022 Tobias Kaminsky * SPDX-FileCopyrightText: 2019-2021 Chris Narkiewicz * SPDX-FileCopyrightText: 2016 Bartosz Przybylski - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.providers; @@ -25,7 +25,6 @@ import android.os.Looper; import android.os.ParcelFileDescriptor; import android.provider.DocumentsContract; import android.provider.DocumentsProvider; -import android.widget.Toast; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; @@ -34,6 +33,8 @@ import com.nextcloud.client.jobs.upload.FileUploadWorker; import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.client.preferences.AppPreferencesImpl; import com.nextcloud.client.utils.HashUtil; +import com.nextcloud.utils.extensions.ContextExtensionsKt; +import com.nextcloud.utils.fileNameValidator.FileNameValidator; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.datamodel.FileDataStorageManager; @@ -47,6 +48,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.files.CheckEtagRemoteOperation; import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation; +import com.owncloud.android.lib.resources.status.OCCapability; import com.owncloud.android.operations.CopyFileOperation; import com.owncloud.android.operations.CreateFolderOperation; import com.owncloud.android.operations.DownloadFileOperation; @@ -59,6 +61,7 @@ import com.owncloud.android.ui.helpers.FileOperationsHelper; import com.owncloud.android.utils.FileStorageUtils; import com.owncloud.android.utils.FileUtil; import com.owncloud.android.utils.MimeTypeUtil; +import com.owncloud.android.utils.theme.CapabilityUtils; import org.nextcloud.providers.cursors.FileCursor; import org.nextcloud.providers.cursors.RootCursor; @@ -95,6 +98,8 @@ public class DocumentsStorageProvider extends DocumentsProvider { @Inject UserAccountManager accountManager; + private boolean isFolderPathValid = true; + @VisibleForTesting static final String DOCUMENTID_SEPARATOR = "/"; private static final int DOCUMENTID_PARTS = 2; @@ -152,17 +157,25 @@ public class DocumentsStorageProvider extends DocumentsProvider { Document parentFolder = toDocument(parentDocumentId); final FileCursor resultCursor = new FileCursor(projection); + if (!parentFolder.getFile().canRead()) { + showToast(R.string.document_storage_provider_cannot_read); + return resultCursor; + } + if (parentFolder.getFile().isEncrypted() && !FileOperationsHelper.isEndToEndEncryptionSetup(context, parentFolder.getUser())) { - Toast.makeText(context, R.string.e2e_not_yet_setup, Toast.LENGTH_LONG).show(); + showToast(R.string.e2e_not_yet_setup); return resultCursor; } FileDataStorageManager storageManager = parentFolder.getStorageManager(); - for (OCFile file : storageManager.getFolderContent(parentFolder.getFile(), false)) { - resultCursor.addFile(new Document(storageManager, file)); + if (file.canRead()) { + resultCursor.addFile(new Document(storageManager, file)); + } else { + Log_OC.w(TAG,"Skipping file, doesn't have read permission. RemotePath: " + file.getRemotePath()); + } } boolean isLoading = false; @@ -187,6 +200,11 @@ public class DocumentsStorageProvider extends DocumentsProvider { throws FileNotFoundException { Log_OC.d(TAG, "openDocument(), id=" + documentId); + if (!isFolderPathValid) { + Log_OC.d(TAG, "Folder path is not valid, operation is cancelled"); + return null; + } + Document document = toDocument(documentId); Context context = getNonNullContext(); @@ -206,13 +224,11 @@ public class DocumentsStorageProvider extends DocumentsProvider { final AtomicBoolean downloadResult = new AtomicBoolean(false); final Thread downloadThread = new Thread(() -> { DownloadFileOperation downloadFileOperation = new DownloadFileOperation(user, ocFile, context); - RemoteOperationResult result = downloadFileOperation.execute(document.getClient()); + final var result = downloadFileOperation.execute(document.getClient()); if (!result.isSuccess()) { if (ocFile.isDown()) { Handler handler = new Handler(Looper.getMainLooper()); - handler.post(() -> Toast.makeText(MainApp.getAppContext(), - R.string.file_not_synced, - Toast.LENGTH_SHORT).show()); + handler.post(() -> showToast(R.string.file_not_synced)); downloadResult.set(true); } else { Log_OC.e(TAG, result.toString()); @@ -275,16 +291,14 @@ public class DocumentsStorageProvider extends DocumentsProvider { OCFile ocFile = document.getFile(); RemoteOperationResult result = new CheckEtagRemoteOperation(ocFile.getRemotePath(), ocFile.getEtag()) .execute(document.getUser(), context); - switch (result.getCode()) { - case ETAG_CHANGED: - return true; - case ETAG_UNCHANGED: - return false; - case FILE_NOT_FOUND: - default: + return switch (result.getCode()) { + case ETAG_CHANGED -> result.getData() != null; + case ETAG_UNCHANGED -> false; + default -> { Log_OC.e(TAG, result.toString()); throw new FileNotFoundException("Error synchronizing file: " + ocFile.getFileName()); - } + } + }; } /** @@ -347,9 +361,19 @@ public class DocumentsStorageProvider extends DocumentsProvider { public String renameDocument(String documentId, String displayName) throws FileNotFoundException { Log_OC.d(TAG, "renameDocument(), id=" + documentId); - Document document = toDocument(documentId); + String errorMessage = checkFileName(displayName); + if (errorMessage != null) { + showToast(errorMessage); + return null; + } - RemoteOperationResult result = new RenameFileOperation(document.getRemotePath(), + Document document = toDocument(documentId); + if (!document.getFile().canRename()) { + showToast(R.string.document_storage_provider_cannot_rename); + return null; + } + + final var result = new RenameFileOperation(document.getRemotePath(), displayName, document.getStorageManager()) .execute(document.getClient()); @@ -370,12 +394,18 @@ public class DocumentsStorageProvider extends DocumentsProvider { public String copyDocument(String sourceDocumentId, String targetParentDocumentId) throws FileNotFoundException { Log_OC.d(TAG, "copyDocument(), id=" + sourceDocumentId); - Document document = toDocument(sourceDocumentId); - - FileDataStorageManager storageManager = document.getStorageManager(); Document targetFolder = toDocument(targetParentDocumentId); - RemoteOperationResult result = new CopyFileOperation(document.getRemotePath(), + String filename = targetFolder.getFile().getFileName(); + isFolderPathValid = checkFolderPath(filename); + if (!isFolderPathValid) { + showToast(R.string.file_name_validator_error_contains_reserved_names_or_invalid_characters); + return null; + } + + Document document = toDocument(sourceDocumentId); + FileDataStorageManager storageManager = document.getStorageManager(); + final var result = new CopyFileOperation(document.getRemotePath(), targetFolder.getRemotePath(), document.getStorageManager()) .execute(document.getClient()); @@ -389,7 +419,7 @@ public class DocumentsStorageProvider extends DocumentsProvider { Context context = getNonNullContext(); User user = document.getUser(); - RemoteOperationResult updateParent = new RefreshFolderOperation(targetFolder.getFile(), + final var updateParent = new RefreshFolderOperation(targetFolder.getFile(), System.currentTimeMillis(), false, false, @@ -422,10 +452,22 @@ public class DocumentsStorageProvider extends DocumentsProvider { throws FileNotFoundException { Log_OC.d(TAG, "moveDocument(), id=" + sourceDocumentId); - Document document = toDocument(sourceDocumentId); Document targetFolder = toDocument(targetParentDocumentId); - RemoteOperationResult result = new MoveFileOperation(document.getRemotePath(), + String filename = targetFolder.getFile().getFileName(); + isFolderPathValid = checkFolderPath(filename); + if (!isFolderPathValid) { + showToast(R.string.file_name_validator_error_contains_reserved_names_or_invalid_characters); + return null; + } + + Document document = toDocument(sourceDocumentId); + if (!document.getFile().canMove()) { + showToast(R.string.document_storage_provider_cannot_move); + return null; + } + + final var result = new MoveFileOperation(document.getRemotePath(), targetFolder.getRemotePath(), document.getStorageManager()) .execute(document.getClient()); @@ -463,11 +505,33 @@ public class DocumentsStorageProvider extends DocumentsProvider { return result; } + private OCCapability getCapabilities() { + return CapabilityUtils.getCapability(accountManager.getUser(), getNonNullContext()); + } + + private boolean checkFolderPath(String filename) { + return FileNameValidator.INSTANCE.checkFolderPath(filename, getCapabilities(), getNonNullContext()); + } + + private String checkFileName(String filename) { + return FileNameValidator.INSTANCE.checkFileName(filename, getCapabilities(), getNonNullContext(),null); + } + @Override public String createDocument(String documentId, String mimeType, String displayName) throws FileNotFoundException { Log_OC.d(TAG, "createDocument(), id=" + documentId); + String errorMessage = checkFileName(displayName); + if (errorMessage != null) { + showToast(errorMessage); + return null; + } + Document folderDocument = toDocument(documentId); + if (!folderDocument.getFile().canCreateFileAndFolder()) { + showToast(R.string.document_storage_provider_cannot_create_file_and_folder); + return null; + } if (DocumentsContract.Document.MIME_TYPE_DIR.equalsIgnoreCase(mimeType)) { return createFolder(folderDocument, displayName); @@ -477,12 +541,16 @@ public class DocumentsStorageProvider extends DocumentsProvider { } private String createFolder(Document targetFolder, String displayName) throws FileNotFoundException { + if (!targetFolder.getFile().canCreateFileAndFolder()) { + showToast(R.string.document_storage_provider_cannot_create_folder_inside_folder); + return null; + } Context context = getNonNullContext(); String newDirPath = targetFolder.getRemotePath() + displayName + PATH_SEPARATOR; FileDataStorageManager storageManager = targetFolder.getStorageManager(); - RemoteOperationResult result = new CreateFolderOperation(newDirPath, + final var result = new CreateFolderOperation(newDirPath, accountManager.getUser(), context, storageManager) @@ -494,7 +562,7 @@ public class DocumentsStorageProvider extends DocumentsProvider { displayName + " and documentId " + targetFolder.getDocumentId()); } - RemoteOperationResult updateParent = new RefreshFolderOperation(targetFolder.getFile(), System.currentTimeMillis(), + final var updateParent = new RefreshFolderOperation(targetFolder.getFile(), System.currentTimeMillis(), false, false, true, storageManager, targetFolder.getUser(), context) .execute(targetFolder.getClient()); @@ -512,6 +580,10 @@ public class DocumentsStorageProvider extends DocumentsProvider { } private String createFile(Document targetFolder, String displayName, String mimeType) throws FileNotFoundException { + if (!targetFolder.getFile().canCreateFileAndFolder()) { + showToast(R.string.document_storage_provider_cannot_create_file_inside_folder); + return null; + } User user = targetFolder.getUser(); @@ -542,7 +614,7 @@ public class DocumentsStorageProvider extends DocumentsProvider { // perform the upload, no need for chunked operation as we have a empty file OwnCloudClient client = targetFolder.getClient(); - RemoteOperationResult result = new UploadFileRemoteOperation(emptyFile.getAbsolutePath(), + final var result = new UploadFileRemoteOperation(emptyFile.getAbsolutePath(), newFilePath, mimeType, "", @@ -558,7 +630,7 @@ public class DocumentsStorageProvider extends DocumentsProvider { Context context = getNonNullContext(); - RemoteOperationResult updateParent = new RefreshFolderOperation(targetFolder.getFile(), + final var updateParent = new RefreshFolderOperation(targetFolder.getFile(), System.currentTimeMillis(), false, false, @@ -592,13 +664,18 @@ public class DocumentsStorageProvider extends DocumentsProvider { Context context = getNonNullContext(); Document document = toDocument(documentId); + if (!document.getFile().canDeleteOrLeaveShare()) { + showToast(R.string.document_storage_provider_cannot_delete); + return; + } + // get parent here, because it is not available anymore after the document was deleted Document parentFolder = document.getParent(); recursiveRevokePermission(document); OCFile file = document.getStorageManager().getFileByPath(document.getRemotePath()); - RemoteOperationResult result = new RemoveFileOperation(file, + final var result = new RemoveFileOperation(file, false, document.getUser(), true, @@ -840,4 +917,12 @@ public class DocumentsStorageProvider extends DocumentsProvider { return new Document(getStorageManager(), parentId); } } + + private void showToast(int messageId) { + ContextExtensionsKt.showToast(getNonNullContext(), messageId); + } + + private void showToast(String message) { + ContextExtensionsKt.showToast(getNonNullContext(), message); + } } diff --git a/app/src/main/java/com/owncloud/android/providers/FileContentProvider.java b/app/src/main/java/com/owncloud/android/providers/FileContentProvider.java index b98db57..19e5fc0 100644 --- a/app/src/main/java/com/owncloud/android/providers/FileContentProvider.java +++ b/app/src/main/java/com/owncloud/android/providers/FileContentProvider.java @@ -7,7 +7,7 @@ * SPDX-FileCopyrightText: 2013-2016 María Asensio Valverde * SPDX-FileCopyrightText: 2014 David A. Velasco * SPDX-FileCopyrightText: 2011 Bartosz Przybylski - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.providers; @@ -120,43 +120,19 @@ public class FileContentProvider extends ContentProvider { VerificationUtils.verifyWhere(where); } - int count; - switch (mUriMatcher.match(uri)) { - case SINGLE_FILE: - count = deleteSingleFile(db, uri, where, whereArgs); - break; - case DIRECTORY: - count = deleteDirectory(db, uri, where, whereArgs); - break; - case ROOT_DIRECTORY: - count = db.delete(ProviderTableMeta.FILE_TABLE_NAME, where, whereArgs); - break; - case SHARES: - count = db.delete(ProviderTableMeta.OCSHARES_TABLE_NAME, where, whereArgs); - break; - case CAPABILITIES: - count = db.delete(ProviderTableMeta.CAPABILITIES_TABLE_NAME, where, whereArgs); - break; - case UPLOADS: - count = db.delete(ProviderTableMeta.UPLOADS_TABLE_NAME, where, whereArgs); - break; - case SYNCED_FOLDERS: - count = db.delete(ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME, where, whereArgs); - break; - case EXTERNAL_LINKS: - count = db.delete(ProviderTableMeta.EXTERNAL_LINKS_TABLE_NAME, where, whereArgs); - break; - case VIRTUAL: - count = db.delete(ProviderTableMeta.VIRTUAL_TABLE_NAME, where, whereArgs); - break; - case FILESYSTEM: - count = db.delete(ProviderTableMeta.FILESYSTEM_TABLE_NAME, where, whereArgs); - break; - default: - throw new IllegalArgumentException(String.format(Locale.US, "Unknown uri: %s", uri.toString())); - } - - return count; + return switch (mUriMatcher.match(uri)) { + case SINGLE_FILE -> deleteSingleFile(db, uri, where, whereArgs); + case DIRECTORY -> deleteDirectory(db, uri, where, whereArgs); + case ROOT_DIRECTORY -> db.delete(ProviderTableMeta.FILE_TABLE_NAME, where, whereArgs); + case SHARES -> db.delete(ProviderTableMeta.OCSHARES_TABLE_NAME, where, whereArgs); + case CAPABILITIES -> db.delete(ProviderTableMeta.CAPABILITIES_TABLE_NAME, where, whereArgs); + case UPLOADS -> db.delete(ProviderTableMeta.UPLOADS_TABLE_NAME, where, whereArgs); + case SYNCED_FOLDERS -> db.delete(ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME, where, whereArgs); + case EXTERNAL_LINKS -> db.delete(ProviderTableMeta.EXTERNAL_LINKS_TABLE_NAME, where, whereArgs); + case VIRTUAL -> db.delete(ProviderTableMeta.VIRTUAL_TABLE_NAME, where, whereArgs); + case FILESYSTEM -> db.delete(ProviderTableMeta.FILESYSTEM_TABLE_NAME, where, whereArgs); + default -> throw new IllegalArgumentException(String.format(Locale.US, "Unknown uri: %s", uri.toString())); + }; } private int deleteDirectory(SupportSQLiteDatabase db, Uri uri, String where, String... whereArgs) { @@ -218,14 +194,11 @@ public class FileContentProvider extends ContentProvider { @Override public String getType(@NonNull Uri uri) { - switch (mUriMatcher.match(uri)) { - case ROOT_DIRECTORY: - return ProviderTableMeta.CONTENT_TYPE; - case SINGLE_FILE: - return ProviderTableMeta.CONTENT_TYPE_ITEM; - default: - throw new IllegalArgumentException(String.format(Locale.US, "Unknown Uri id: %s", uri)); - } + return switch (mUriMatcher.match(uri)) { + case ROOT_DIRECTORY -> ProviderTableMeta.CONTENT_TYPE; + case SINGLE_FILE -> ProviderTableMeta.CONTENT_TYPE_ITEM; + default -> throw new IllegalArgumentException(String.format(Locale.US, "Unknown Uri id: %s", uri)); + }; } @Override @@ -372,7 +345,13 @@ public class FileContentProvider extends ContentProvider { private void updateFilesTableAccordingToShareInsertion(SupportSQLiteDatabase db, ContentValues newShare) { ContentValues fileValues = new ContentValues(); - ShareType newShareType = ShareType.fromValue(newShare.getAsInteger(ProviderTableMeta.OCSHARES_SHARE_TYPE)); + Integer shareTypeValue = newShare.getAsInteger(ProviderTableMeta.OCSHARES_SHARE_TYPE); + if (shareTypeValue == null) { + Log_OC.w(TAG, "Share type is null. Skipping file update."); + return; + } + + ShareType newShareType = ShareType.fromValue(shareTypeValue); switch (newShareType) { case PUBLIC_LINK: @@ -474,38 +453,20 @@ public class FileContentProvider extends ContentProvider { // verify only for those requests that are not internal final int uriMatch = mUriMatcher.match(uri); - String tableName; - switch (uriMatch) { - case ROOT_DIRECTORY: - case DIRECTORY: - case SINGLE_FILE: + String tableName = switch (uriMatch) { + case ROOT_DIRECTORY, DIRECTORY, SINGLE_FILE -> { VerificationUtils.verifyWhere(selection); // prevent injection in public paths - tableName = ProviderTableMeta.FILE_TABLE_NAME; - break; - case SHARES: - tableName = ProviderTableMeta.OCSHARES_TABLE_NAME; - break; - case CAPABILITIES: - tableName = ProviderTableMeta.CAPABILITIES_TABLE_NAME; - break; - case UPLOADS: - tableName = ProviderTableMeta.UPLOADS_TABLE_NAME; - break; - case SYNCED_FOLDERS: - tableName = ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME; - break; - case EXTERNAL_LINKS: - tableName = ProviderTableMeta.EXTERNAL_LINKS_TABLE_NAME; - break; - case VIRTUAL: - tableName = ProviderTableMeta.VIRTUAL_TABLE_NAME; - break; - case FILESYSTEM: - tableName = ProviderTableMeta.FILESYSTEM_TABLE_NAME; - break; - default: - throw new IllegalArgumentException("Unknown uri id: " + uri); - } + yield ProviderTableMeta.FILE_TABLE_NAME; + } + case SHARES -> ProviderTableMeta.OCSHARES_TABLE_NAME; + case CAPABILITIES -> ProviderTableMeta.CAPABILITIES_TABLE_NAME; + case UPLOADS -> ProviderTableMeta.UPLOADS_TABLE_NAME; + case SYNCED_FOLDERS -> ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME; + case EXTERNAL_LINKS -> ProviderTableMeta.EXTERNAL_LINKS_TABLE_NAME; + case VIRTUAL -> ProviderTableMeta.VIRTUAL_TABLE_NAME; + case FILESYSTEM -> ProviderTableMeta.FILESYSTEM_TABLE_NAME; + default -> throw new IllegalArgumentException("Unknown uri id: " + uri); + }; SupportSQLiteQueryBuilder queryBuilder = SupportSQLiteQueryBuilder.builder(tableName); @@ -520,32 +481,17 @@ public class FileContentProvider extends ContentProvider { String order; if (TextUtils.isEmpty(sortOrder)) { - switch (uriMatch) { - case SHARES: - order = ProviderTableMeta.OCSHARES_DEFAULT_SORT_ORDER; - break; - case CAPABILITIES: - order = ProviderTableMeta.CAPABILITIES_DEFAULT_SORT_ORDER; - break; - case UPLOADS: - order = ProviderTableMeta.UPLOADS_DEFAULT_SORT_ORDER; - break; - case SYNCED_FOLDERS: - order = ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH; - break; - case EXTERNAL_LINKS: - order = ProviderTableMeta.EXTERNAL_LINKS_NAME; - break; - case VIRTUAL: - order = ProviderTableMeta.VIRTUAL_TYPE; - break; - default: // Files - order = ProviderTableMeta.FILE_DEFAULT_SORT_ORDER; - break; - case FILESYSTEM: - order = ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH; - break; - } + order = switch (uriMatch) { + case SHARES -> ProviderTableMeta.OCSHARES_DEFAULT_SORT_ORDER; + case CAPABILITIES -> ProviderTableMeta.CAPABILITIES_DEFAULT_SORT_ORDER; + case UPLOADS -> ProviderTableMeta.UPLOADS_DEFAULT_SORT_ORDER; + case SYNCED_FOLDERS -> ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH; + case EXTERNAL_LINKS -> ProviderTableMeta.EXTERNAL_LINKS_NAME; + case VIRTUAL -> ProviderTableMeta.VIRTUAL_TYPE; + case FILESYSTEM -> ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH; + default -> // Files + ProviderTableMeta.FILE_DEFAULT_SORT_ORDER; + }; } else { if (uriMatch == ROOT_DIRECTORY || uriMatch == SINGLE_FILE || uriMatch == DIRECTORY) { VerificationUtils.verifySortOrder(sortOrder); @@ -614,22 +560,21 @@ public class FileContentProvider extends ContentProvider { VerificationUtils.verifyWhere(selection); } - switch (mUriMatcher.match(uri)) { - case DIRECTORY: - return 0; - case SHARES: - return db.update(ProviderTableMeta.OCSHARES_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values, selection, selectionArgs); - case CAPABILITIES: - return db.update(ProviderTableMeta.CAPABILITIES_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values, selection, selectionArgs); - case UPLOADS: - return db.update(ProviderTableMeta.UPLOADS_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values, selection, selectionArgs); - case SYNCED_FOLDERS: - return db.update(ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values, selection, selectionArgs); - case FILESYSTEM: - return db.update(ProviderTableMeta.FILESYSTEM_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values, selection, selectionArgs); - default: - return db.update(ProviderTableMeta.FILE_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values, selection, selectionArgs); - } + return switch (mUriMatcher.match(uri)) { + case DIRECTORY -> 0; + case SHARES -> + db.update(ProviderTableMeta.OCSHARES_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values, selection, selectionArgs); + case CAPABILITIES -> + db.update(ProviderTableMeta.CAPABILITIES_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values, selection, selectionArgs); + case UPLOADS -> + db.update(ProviderTableMeta.UPLOADS_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values, selection, selectionArgs); + case SYNCED_FOLDERS -> + db.update(ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values, selection, selectionArgs); + case FILESYSTEM -> + db.update(ProviderTableMeta.FILESYSTEM_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values, selection, selectionArgs); + default -> + db.update(ProviderTableMeta.FILE_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values, selection, selectionArgs); + }; } @NonNull @@ -657,23 +602,13 @@ public class FileContentProvider extends ContentProvider { } private boolean isCallerNotAllowed(Uri uri) { - switch (mUriMatcher.match(uri)) { - case SHARES: - case CAPABILITIES: - case UPLOADS: - case SYNCED_FOLDERS: - case EXTERNAL_LINKS: - case VIRTUAL: - case FILESYSTEM: + return switch (mUriMatcher.match(uri)) { + case SHARES, CAPABILITIES, UPLOADS, SYNCED_FOLDERS, EXTERNAL_LINKS, VIRTUAL, FILESYSTEM -> { String callingPackage = mContext.getPackageManager().getNameForUid(Binder.getCallingUid()); - return callingPackage == null || !callingPackage.equals(mContext.getPackageName()); - - case ROOT_DIRECTORY: - case SINGLE_FILE: - case DIRECTORY: - default: - return false; - } + yield callingPackage == null || !callingPackage.equals(mContext.getPackageName()); + } + default -> false; + }; } diff --git a/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchConfig.kt b/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchConfig.kt index 776d549..dc38546 100644 --- a/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchConfig.kt +++ b/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchConfig.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Álvaro Brey * SPDX-FileCopyrightText: 2023 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.providers diff --git a/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java b/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java index cb27855..ef8df0f 100644 --- a/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java +++ b/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java @@ -6,7 +6,7 @@ * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2016 Juan Carlos González Cabrero * SPDX-FileCopyrightText: 2015 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.providers; @@ -100,6 +100,7 @@ public class UsersAndGroupsSearchProvider extends ContentProvider { private String DATA_GROUP; private String DATA_ROOM; private String DATA_REMOTE; + private String DATA_REMOTE_GROUP; private String DATA_EMAIL; private String DATA_CIRCLE; @@ -142,6 +143,7 @@ public class UsersAndGroupsSearchProvider extends ContentProvider { DATA_GROUP = AUTHORITY + ".data.group"; DATA_ROOM = AUTHORITY + ".data.room"; DATA_REMOTE = AUTHORITY + ".data.remote"; + DATA_REMOTE_GROUP = AUTHORITY + ".data.remote_group"; DATA_EMAIL = AUTHORITY + ".data.email"; DATA_CIRCLE = AUTHORITY + ".data.circle"; @@ -149,6 +151,7 @@ public class UsersAndGroupsSearchProvider extends ContentProvider { sShareTypes.put(DATA_GROUP, ShareType.GROUP); sShareTypes.put(DATA_ROOM, ShareType.ROOM); sShareTypes.put(DATA_REMOTE, ShareType.FEDERATED); + sShareTypes.put(DATA_REMOTE_GROUP, ShareType.FEDERATED_GROUP); sShareTypes.put(DATA_EMAIL, ShareType.EMAIL); sShareTypes.put(DATA_CIRCLE, ShareType.CIRCLE); @@ -228,6 +231,7 @@ public class UsersAndGroupsSearchProvider extends ContentProvider { Uri groupBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_GROUP).build(); Uri roomBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_ROOM).build(); Uri remoteBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_REMOTE).build(); + Uri remoteGroupBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_REMOTE_GROUP).build(); Uri emailBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_EMAIL).build(); Uri circleBaseUri = new Uri.Builder().scheme(CONTENT).authority(DATA_CIRCLE).build(); @@ -282,7 +286,7 @@ public class UsersAndGroupsSearchProvider extends ContentProvider { case FEDERATED: if (federatedShareAllowed) { - icon = R.drawable.ic_user; + icon = R.drawable.ic_user_outline; dataUri = Uri.withAppendedPath(remoteBaseUri, shareWith); if (userName.equals(shareWith)) { @@ -297,6 +301,24 @@ public class UsersAndGroupsSearchProvider extends ContentProvider { } break; + case FEDERATED_GROUP: + if (federatedShareAllowed) { + icon = R.drawable.ic_group; + dataUri = Uri.withAppendedPath(remoteGroupBaseUri, shareWith); + + if (userName.equals(shareWith)) { + displayName = name; + subline = getContext().getString(R.string.remote); + subline = ""; + } else { + String[] uriSplitted = shareWith.split("@"); + displayName = name; + subline = getContext().getString(R.string.share_known_remote_on_clarification, + uriSplitted[uriSplitted.length - 1]); + } + } + break; + case USER: displayName = userName; subline = (status.getMessage() == null || status.getMessage().isEmpty()) ? null : diff --git a/app/src/main/java/com/owncloud/android/services/AccountManagerService.java b/app/src/main/java/com/owncloud/android/services/AccountManagerService.java index 6b4245e..05d7b54 100644 --- a/app/src/main/java/com/owncloud/android/services/AccountManagerService.java +++ b/app/src/main/java/com/owncloud/android/services/AccountManagerService.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2017 David Luhmer - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.services; diff --git a/app/src/main/java/com/owncloud/android/services/OperationsService.java b/app/src/main/java/com/owncloud/android/services/OperationsService.java index c2b0610..6923606 100644 --- a/app/src/main/java/com/owncloud/android/services/OperationsService.java +++ b/app/src/main/java/com/owncloud/android/services/OperationsService.java @@ -8,7 +8,7 @@ * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2015 María Asensio Valverde * SPDX-FileCopyrightText: 2014 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.services; @@ -29,6 +29,7 @@ import android.util.Pair; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; +import com.nextcloud.common.NextcloudClient; import com.nextcloud.utils.extensions.IntentExtensionsKt; import com.owncloud.android.MainApp; import com.owncloud.android.datamodel.ArbitraryDataProvider; @@ -55,6 +56,7 @@ import com.owncloud.android.operations.GetServerInfoOperation; import com.owncloud.android.operations.MoveFileOperation; import com.owncloud.android.operations.RemoveFileOperation; import com.owncloud.android.operations.RenameFileOperation; +import com.owncloud.android.operations.SetFilesDownloadLimitOperation; import com.owncloud.android.operations.SynchronizeFileOperation; import com.owncloud.android.operations.SynchronizeFolderOperation; import com.owncloud.android.operations.UnshareOperation; @@ -64,7 +66,6 @@ import com.owncloud.android.operations.UpdateSharePermissionsOperation; import com.owncloud.android.operations.UpdateShareViaLinkOperation; import java.io.IOException; -import java.util.Iterator; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; @@ -81,6 +82,7 @@ public class OperationsService extends Service { private static final String TAG = OperationsService.class.getSimpleName(); public static final String EXTRA_ACCOUNT = "ACCOUNT"; + public static final String EXTRA_POST_DIALOG_EVENT = "EXTRA_POST_DIALOG_EVENT"; public static final String EXTRA_SERVER_URL = "SERVER_URL"; public static final String EXTRA_REMOTE_PATH = "REMOTE_PATH"; public static final String EXTRA_NEWNAME = "NEWNAME"; @@ -97,8 +99,11 @@ public class OperationsService extends Service { public static final String EXTRA_SHARE_PUBLIC_LABEL = "SHARE_PUBLIC_LABEL"; public static final String EXTRA_SHARE_HIDE_FILE_DOWNLOAD = "HIDE_FILE_DOWNLOAD"; public static final String EXTRA_SHARE_ID = "SHARE_ID"; + public static final String EXTRA_SHARE_REMOTE_ID = "SHARE_REMOTE_ID"; public static final String EXTRA_SHARE_NOTE = "SHARE_NOTE"; public static final String EXTRA_IN_BACKGROUND = "IN_BACKGROUND"; + public static final String EXTRA_FILES_DOWNLOAD_LIMIT = "FILES_DOWNLOAD_LIMIT"; + public static final String EXTRA_SHARE_ATTRIBUTES = "SHARE_ATTRIBUTES"; public static final String ACTION_CREATE_SHARE_VIA_LINK = "CREATE_SHARE_VIA_LINK"; public static final String ACTION_CREATE_SECURE_FILE_DROP = "CREATE_SECURE_FILE_DROP"; @@ -119,6 +124,7 @@ public class OperationsService extends Service { public static final String ACTION_COPY_FILE = "COPY_FILE"; public static final String ACTION_CHECK_CURRENT_CREDENTIALS = "CHECK_CURRENT_CREDENTIALS"; public static final String ACTION_RESTORE_VERSION = "RESTORE_VERSION"; + public static final String ACTION_UPDATE_FILES_DOWNLOAD_LIMIT = "UPDATE_FILES_DOWNLOAD_LIMIT"; private ServiceHandler mOperationsHandler; private OperationsServiceBinder mOperationsBinder; @@ -260,7 +266,7 @@ public class OperationsService extends Service { */ private final ConcurrentMap mBoundListeners = new ConcurrentHashMap<>(); - private ServiceHandler mServiceHandler; + private final ServiceHandler mServiceHandler; public OperationsServiceBinder(ServiceHandler serviceHandler) { mServiceHandler = serviceHandler; @@ -380,7 +386,7 @@ public class OperationsService extends Service { OperationsService mService; - private ConcurrentLinkedQueue> mPendingOperations = + private final ConcurrentLinkedQueue> mPendingOperations = new ConcurrentLinkedQueue<>(); private RemoteOperation mCurrentOperation; private Target mLastTarget; @@ -416,11 +422,12 @@ public class OperationsService extends Service { if (next != null) { mCurrentOperation = next.second; RemoteOperationResult result; + OwnCloudAccount ocAccount = null; + try { /// prepare client object to send the request to the ownCloud server if (mLastTarget == null || !mLastTarget.equals(next.first)) { mLastTarget = next.first; - OwnCloudAccount ocAccount; if (mLastTarget.mAccount != null) { ocAccount = new OwnCloudAccount(mLastTarget.mAccount, mService); } else { @@ -430,9 +437,26 @@ public class OperationsService extends Service { getClientFor(ocAccount, mService); } - /// perform the operation - result = mCurrentOperation.execute(mOwnCloudClient); - } catch (AccountsException e) { + // perform the operation + try { + result = mCurrentOperation.execute(mOwnCloudClient); + if (!result.isSuccess()) { + final var code = "code: " + result.getCode(); + final var httpCode = "HTTP_CODE: " + result.getHttpCode(); + Log_OC.e(TAG,"Operation failed " + code + httpCode); + } + } catch (UnsupportedOperationException e) { + // TODO remove - added to aid in transition to NextcloudClient + + if (ocAccount == null) { + throw e; + } + + NextcloudClient nextcloudClient = OwnCloudClientManagerFactory.getDefaultSingleton() + .getNextcloudClientFor(ocAccount, mService.getBaseContext()); + result = mCurrentOperation.run(nextcloudClient); + } + } catch (AccountsException | IOException e) { if (mLastTarget.mAccount == null) { Log_OC.e(TAG, "Error while trying to get authorization for a NULL account", e); @@ -442,15 +466,6 @@ public class OperationsService extends Service { } result = new RemoteOperationResult(e); - } catch (IOException e) { - if (mLastTarget.mAccount == null) { - Log_OC.e(TAG, "Error while trying to get authorization for a NULL account", - e); - } else { - Log_OC.e(TAG, "Error while trying to get authorization for " + - mLastTarget.mAccount.name, e); - } - result = new RemoteOperationResult(e); } catch (Exception e) { if (mLastTarget.mAccount == null) { Log_OC.e(TAG, "Unexpected error for a NULL account", e); @@ -537,10 +552,6 @@ public class OperationsService extends Service { false); updateLinkOperation.setHideFileDownload(hideFileDownload); -// if (operationIntent.hasExtra(EXTRA_SHARE_PUBLIC_UPLOAD)) { -// updateLinkOperation.setPublicUpload(true); -// } - if (operationIntent.hasExtra(EXTRA_SHARE_PUBLIC_LABEL)) { updateLinkOperation.setLabel(operationIntent.getStringExtra(EXTRA_SHARE_PUBLIC_LABEL)); } @@ -590,6 +601,8 @@ public class OperationsService extends Service { .getLongExtra(EXTRA_SHARE_EXPIRATION_DATE_IN_MILLIS, 0L); boolean hideFileDownload = operationIntent.getBooleanExtra(EXTRA_SHARE_HIDE_FILE_DOWNLOAD, false); + String attributes = operationIntent.getStringExtra(EXTRA_SHARE_ATTRIBUTES); + if (!TextUtils.isEmpty(remotePath)) { CreateShareWithShareeOperation createShareWithShareeOperation = new CreateShareWithShareeOperation(remotePath, @@ -600,6 +613,7 @@ public class OperationsService extends Service { sharePassword, expirationDateInMillis, hideFileDownload, + attributes, fileDataStorageManager, getApplicationContext(), user, @@ -614,9 +628,11 @@ public class OperationsService extends Service { case ACTION_UPDATE_SHARE_INFO: shareId = operationIntent.getLongExtra(EXTRA_SHARE_ID, -1); + long shareRemoteId = operationIntent.getLongExtra(EXTRA_SHARE_REMOTE_ID, -1); - if (shareId > 0) { + if (shareId > 0 || shareRemoteId > 0) { UpdateShareInfoOperation updateShare = new UpdateShareInfoOperation(shareId, + shareRemoteId, fileDataStorageManager); int permissionsToChange = operationIntent.getIntExtra(EXTRA_SHARE_PERMISSIONS, -1); @@ -638,6 +654,9 @@ public class OperationsService extends Service { updateShare.setLabel(operationIntent.getStringExtra(EXTRA_SHARE_PUBLIC_LABEL)); } + String shareAttributes = operationIntent.getStringExtra(EXTRA_SHARE_ATTRIBUTES); + updateShare.setAttributes(shareAttributes); + operation = updateShare; } break; @@ -672,6 +691,11 @@ public class OperationsService extends Service { case ACTION_REMOVE: // Remove file or folder OCFile file = IntentExtensionsKt.getParcelableArgument(operationIntent, EXTRA_FILE, OCFile.class); + if (file == null) { + Log_OC.w(TAG, "file is null cannot remove file"); + break; + } + boolean onlyLocalCopy = operationIntent.getBooleanExtra(EXTRA_REMOVE_ONLY_LOCAL, false); boolean inBackground = operationIntent.getBooleanExtra(EXTRA_IN_BACKGROUND, false); operation = new RemoveFileOperation(file, @@ -692,12 +716,15 @@ public class OperationsService extends Service { case ACTION_SYNC_FILE: remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); + boolean postDialogEvent = operationIntent.getBooleanExtra(EXTRA_POST_DIALOG_EVENT, true); boolean syncFileContents = operationIntent.getBooleanExtra(EXTRA_SYNC_FILE_CONTENTS, true); operation = new SynchronizeFileOperation(remotePath, user, syncFileContents, getApplicationContext(), - fileDataStorageManager); + fileDataStorageManager, + false, + postDialogEvent); break; case ACTION_SYNC_FOLDER: @@ -706,8 +733,8 @@ public class OperationsService extends Service { this, // TODO remove this dependency from construction time remotePath, user, - System.currentTimeMillis(), // TODO remove this dependency from construction time - fileDataStorageManager + fileDataStorageManager, + false ); break; @@ -729,10 +756,24 @@ public class OperationsService extends Service { case ACTION_RESTORE_VERSION: FileVersion fileVersion = IntentExtensionsKt.getParcelableArgument(operationIntent, EXTRA_FILE_VERSION, FileVersion.class); + if (fileVersion == null) { + Log_OC.w(TAG, "file version is null cannot restore file"); + break; + } + operation = new RestoreFileVersionRemoteOperation(fileVersion.getLocalId(), fileVersion.getFileName()); break; + case ACTION_UPDATE_FILES_DOWNLOAD_LIMIT: + shareId = operationIntent.getLongExtra(EXTRA_SHARE_ID, -1); + int newLimit = operationIntent.getIntExtra(EXTRA_FILES_DOWNLOAD_LIMIT, -1); + + if (shareId > 0) { + operation = new SetFilesDownloadLimitOperation(shareId, newLimit, fileDataStorageManager, getApplicationContext()); + } + break; + default: // do nothing break; diff --git a/app/src/main/java/com/owncloud/android/services/SyncFolderHandler.java b/app/src/main/java/com/owncloud/android/services/SyncFolderHandler.java index 917b79a..4c8d75b 100644 --- a/app/src/main/java/com/owncloud/android/services/SyncFolderHandler.java +++ b/app/src/main/java/com/owncloud/android/services/SyncFolderHandler.java @@ -7,7 +7,7 @@ * SPDX-FileCopyrightText: 2016 ownCloud Inc. * SPDX-FileCopyrightText: 2015 María Asensio Valverde * SPDX-FileCopyrightText: 2015 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.services; diff --git a/app/src/main/java/com/owncloud/android/syncadapter/AbstractOwnCloudSyncAdapter.java b/app/src/main/java/com/owncloud/android/syncadapter/AbstractOwnCloudSyncAdapter.java index 3956b84..42258de 100644 --- a/app/src/main/java/com/owncloud/android/syncadapter/AbstractOwnCloudSyncAdapter.java +++ b/app/src/main/java/com/owncloud/android/syncadapter/AbstractOwnCloudSyncAdapter.java @@ -9,7 +9,7 @@ * SPDX-FileCopyrightText: 2015 David A. Velasco * SPDX-FileCopyrightText: 2011-2012 Bartosz Przybylski * SPDX-FileCopyrightText: 2011 Sven Aßmann - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.syncadapter; diff --git a/app/src/main/java/com/owncloud/android/syncadapter/FileSyncAdapter.java b/app/src/main/java/com/owncloud/android/syncadapter/FileSyncAdapter.java index 1dc10c9..5a8b609 100644 --- a/app/src/main/java/com/owncloud/android/syncadapter/FileSyncAdapter.java +++ b/app/src/main/java/com/owncloud/android/syncadapter/FileSyncAdapter.java @@ -9,7 +9,7 @@ * SPDX-FileCopyrightText: 2013-2015 David A. Velasco * SPDX-FileCopyrightText: 2011-2012 Bartosz Przybylski * SPDX-FileCopyrightText: 2011 Sven Aßmann - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.syncadapter; @@ -153,7 +153,7 @@ public class FileSyncAdapter extends AbstractOwnCloudSyncAdapter { mLastFailedResult = null; mConflictsFound = 0; mFailsInFavouritesFound = 0; - mForgottenLocalFiles = new HashMap(); + mForgottenLocalFiles = new HashMap<>(); mSyncResult = syncResult; mSyncResult.fullSyncRequested = false; mSyncResult.delayUntil = (System.currentTimeMillis()/1000) + 3*60*60; // avoid too many automatic synchronizations @@ -481,10 +481,8 @@ public class FileSyncAdapter extends AbstractOwnCloudSyncAdapter { /// includes a pending intent in the notification showing a more detailed explanation Intent explanationIntent = new Intent(getContext(), ErrorsWhileCopyingHandlerActivity.class); explanationIntent.putExtra(ErrorsWhileCopyingHandlerActivity.EXTRA_USER, getUser()); - ArrayList remotePaths = new ArrayList(); - ArrayList localPaths = new ArrayList(); - remotePaths.addAll(mForgottenLocalFiles.keySet()); - localPaths.addAll(mForgottenLocalFiles.values()); + ArrayList remotePaths = new ArrayList<>(mForgottenLocalFiles.keySet()); + ArrayList localPaths = new ArrayList<>(mForgottenLocalFiles.values()); explanationIntent.putExtra(ErrorsWhileCopyingHandlerActivity.EXTRA_LOCAL_PATHS, localPaths); explanationIntent.putExtra(ErrorsWhileCopyingHandlerActivity.EXTRA_REMOTE_PATHS, remotePaths); explanationIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); @@ -525,11 +523,7 @@ public class FileSyncAdapter extends AbstractOwnCloudSyncAdapter { private void showNotification(int id, NotificationCompat.Builder builder) { NotificationManager notificationManager = (NotificationManager) getContext(). getSystemService(Context.NOTIFICATION_SERVICE); - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - builder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_FILE_SYNC); - } - + builder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_FILE_SYNC); notificationManager.notify(id, builder.build()); } /** diff --git a/app/src/main/java/com/owncloud/android/syncadapter/FileSyncService.java b/app/src/main/java/com/owncloud/android/syncadapter/FileSyncService.java index 030c6c3..50dcfe8 100644 --- a/app/src/main/java/com/owncloud/android/syncadapter/FileSyncService.java +++ b/app/src/main/java/com/owncloud/android/syncadapter/FileSyncService.java @@ -7,7 +7,7 @@ * SPDX-FileCopyrightText: 2013 David A. Velasco * SPDX-FileCopyrightText: 2011-2012 Bartosz Przybylski * SPDX-FileCopyrightText: 2011 Sven Aßmann - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.syncadapter; diff --git a/app/src/main/java/com/owncloud/android/ui/AvatarGroupLayout.java b/app/src/main/java/com/owncloud/android/ui/AvatarGroupLayout.java deleted file mode 100644 index 5485b55..0000000 --- a/app/src/main/java/com/owncloud/android/ui/AvatarGroupLayout.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Andy Scherzinger - * @author Stefan Niedermann - * Copyright (C) 2021 Andy Scherzinger - * Copyright (C) 2021 Stefan Niedermann - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.owncloud.android.ui; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.RelativeLayout; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.target.BitmapImageViewTarget; -import com.nextcloud.client.account.User; -import com.owncloud.android.R; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.shares.ShareeUser; -import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import java.util.List; - -import androidx.annotation.NonNull; -import androidx.annotation.Px; -import androidx.core.content.ContextCompat; -import androidx.core.content.res.ResourcesCompat; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.core.graphics.drawable.RoundedBitmapDrawable; -import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; - -public class AvatarGroupLayout extends RelativeLayout implements DisplayUtils.AvatarGenerationListener { - private static final String TAG = AvatarGroupLayout.class.getSimpleName(); - - private final static int MAX_AVATAR_COUNT = 3; - - private final Drawable borderDrawable; - @Px private final int avatarSize; - @Px private final int avatarBorderSize; - @Px private final int overlapPx; - - public AvatarGroupLayout(Context context) { - this(context, null); - } - - public AvatarGroupLayout(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public AvatarGroupLayout(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public AvatarGroupLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - avatarBorderSize = DisplayUtils.convertDpToPixel(2, context); - avatarSize = DisplayUtils.convertDpToPixel(40, context); - overlapPx = DisplayUtils.convertDpToPixel(24, context); - borderDrawable = ContextCompat.getDrawable(context, R.drawable.round_bgnd); - assert borderDrawable != null; - DrawableCompat.setTint(borderDrawable, ContextCompat.getColor(context, R.color.bg_default)); - } - - public void setAvatars(@NonNull User user, - @NonNull List sharees, - final ViewThemeUtils viewThemeUtils) { - @NonNull Context context = getContext(); - removeAllViews(); - RelativeLayout.LayoutParams avatarLayoutParams; - int avatarCount; - int shareeSize = Math.min(sharees.size(), MAX_AVATAR_COUNT); - - Resources resources = context.getResources(); - float avatarRadius = resources.getDimension(R.dimen.list_item_avatar_icon_radius); - ShareeUser sharee; - - for (avatarCount = 0; avatarCount < shareeSize; avatarCount++) { - avatarLayoutParams = new RelativeLayout.LayoutParams(avatarSize, avatarSize); - avatarLayoutParams.setMargins(0, 0, avatarCount * overlapPx, 0); - avatarLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); - - final ImageView avatar = new ImageView(context); - avatar.setLayoutParams(avatarLayoutParams); - avatar.setPadding(avatarBorderSize, avatarBorderSize, avatarBorderSize, avatarBorderSize); - - avatar.setBackground(borderDrawable); - addView(avatar); - avatar.requestLayout(); - - if (avatarCount == 0 && sharees.size() > MAX_AVATAR_COUNT) { - avatar.setImageResource(R.drawable.ic_people); - viewThemeUtils.platform.tintTextDrawable(context, avatar.getDrawable()); - } else { - sharee = sharees.get(avatarCount); - switch (sharee.getShareType()) { - case GROUP: - case EMAIL: - case ROOM: - case CIRCLE: - viewThemeUtils.files.createAvatar(sharee.getShareType(), avatar, context); - break; - case FEDERATED: - showFederatedShareAvatar(context, - sharee.getUserId(), - avatarRadius, - resources, - avatar, - viewThemeUtils); - break; - default: - avatar.setTag(sharee); - DisplayUtils.setAvatar(user, - sharee.getUserId(), - sharee.getDisplayName(), - this, - avatarRadius, - resources, - avatar, - context); - break; - } - } - } - - // Recalculate container size based on avatar count - int size = overlapPx * (avatarCount - 1) + avatarSize; - ViewGroup.LayoutParams rememberParam = getLayoutParams(); - rememberParam.width = size; - setLayoutParams(rememberParam); - } - - private void showFederatedShareAvatar(Context context, - String user, - float avatarRadius, - Resources resources, - ImageView avatar, - ViewThemeUtils viewThemeUtils) { - // maybe federated share - String[] split = user.split("@"); - String userId = split[0]; - String server = split[1]; - - String url = "https://" + server + "/index.php/avatar/" + userId + "/" + - resources.getInteger(R.integer.file_avatar_px); - - Drawable placeholder; - try { - placeholder = TextDrawable.createAvatarByUserId(userId, avatarRadius); - } catch (Exception e) { - Log_OC.e(TAG, "Error calculating RGB value for active account icon.", e); - placeholder = viewThemeUtils.platform.colorDrawable(ResourcesCompat.getDrawable(resources, - R.drawable.account_circle_white, - null), - ContextCompat.getColor(context, R.color.black)); - } - - avatar.setTag(null); - Glide.with(context).load(url) - .asBitmap() - .placeholder(placeholder) - .error(placeholder) - .into(new BitmapImageViewTarget(avatar) { - @Override - protected void setResource(Bitmap resource) { - RoundedBitmapDrawable circularBitmapDrawable = RoundedBitmapDrawableFactory.create(resources, - resource); - circularBitmapDrawable.setCircular(true); - avatar.setImageDrawable(circularBitmapDrawable); - } - }); - } - - @Override - public void avatarGenerated(Drawable avatarDrawable, Object callContext) { - ((ImageView) callContext).setImageDrawable(avatarDrawable); - } - - @Override - public boolean shouldCallGeneratedCallback(String tag, Object callContext) { - return ((ImageView) callContext).getTag().equals(tag); - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/AvatarGroupLayout.kt b/app/src/main/java/com/owncloud/android/ui/AvatarGroupLayout.kt new file mode 100644 index 0000000..ab22b2c --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/AvatarGroupLayout.kt @@ -0,0 +1,182 @@ +/* + * Nextcloud Android client application + * + * @author Andy Scherzinger + * @author Stefan Niedermann + * Copyright (C) 2021 Andy Scherzinger + * Copyright (C) 2021 Stefan Niedermann + * + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only + */ +package com.owncloud.android.ui + +import android.content.Context +import android.content.res.Resources +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.widget.ImageView +import android.widget.RelativeLayout +import androidx.annotation.Px +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.graphics.drawable.DrawableCompat +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.client.account.User +import com.nextcloud.utils.GlideHelper.loadCircularBitmapIntoImageView +import com.owncloud.android.R +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.shares.ShareType +import com.owncloud.android.lib.resources.shares.ShareeUser +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.DisplayUtils.AvatarGenerationListener +import com.owncloud.android.utils.theme.ViewThemeUtils +import kotlin.math.min + +@Suppress("MagicNumber") +class AvatarGroupLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : RelativeLayout(context, attrs, defStyleAttr, defStyleRes), + AvatarGenerationListener { + private val borderDrawable = ContextCompat.getDrawable(context, R.drawable.round_bgnd) + + @Px + private val avatarSize: Int = DisplayUtils.convertDpToPixel(40f, context) + + @Px + private val avatarBorderSize: Int = DisplayUtils.convertDpToPixel(2f, context) + + @Px + private val overlapPx: Int = DisplayUtils.convertDpToPixel(24f, context) + + init { + checkNotNull(borderDrawable) + DrawableCompat.setTint(borderDrawable, ContextCompat.getColor(context, R.color.bg_default)) + } + + @Suppress("LongMethod", "TooGenericExceptionCaught") + fun setAvatars(user: User, sharees: MutableList, viewThemeUtils: ViewThemeUtils) { + val context = getContext() + removeAllViews() + var avatarLayoutParams: LayoutParams? + val shareeSize = min(sharees.size, MAX_AVATAR_COUNT) + val resources = context.resources + val avatarRadius = resources.getDimension(R.dimen.list_item_avatar_icon_radius) + var sharee: ShareeUser + + var avatarCount = 0 + while (avatarCount < shareeSize) { + avatarLayoutParams = LayoutParams(avatarSize, avatarSize).apply { + setMargins(0, 0, avatarCount * overlapPx, 0) + addRule(ALIGN_PARENT_RIGHT) + } + + val avatar = ImageView(context).apply { + layoutParams = avatarLayoutParams + setPadding(avatarBorderSize, avatarBorderSize, avatarBorderSize, avatarBorderSize) + background = borderDrawable + } + + addView(avatar) + avatar.requestLayout() + + if (avatarCount == 0 && sharees.size > MAX_AVATAR_COUNT) { + avatar.setImageResource(R.drawable.ic_people) + viewThemeUtils.platform.tintDrawable(context, avatar.drawable, ColorRole.ON_SURFACE) + } else { + sharee = sharees[avatarCount] + when (sharee.shareType) { + ShareType.GROUP, ShareType.EMAIL, ShareType.ROOM, ShareType.CIRCLE -> + viewThemeUtils.files.createAvatar( + sharee.shareType, + avatar, + context + ) + + ShareType.FEDERATED, ShareType.FEDERATED_GROUP -> showFederatedShareAvatar( + context, + sharee.userId!!, + avatarRadius, + resources, + avatar, + viewThemeUtils + ) + + else -> { + avatar.tag = sharee + DisplayUtils.setAvatar( + user, + sharee.userId!!, + sharee.displayName, + this, + avatarRadius, + resources, + avatar, + context + ) + } + } + } + avatarCount++ + } + + // Recalculate container size based on avatar count + val size = overlapPx * (avatarCount - 1) + avatarSize + val rememberParam = layoutParams + rememberParam.width = size + layoutParams = rememberParam + } + + @Suppress("TooGenericExceptionCaught") + private fun showFederatedShareAvatar( + context: Context, + user: String, + avatarRadius: Float, + resources: Resources, + avatar: ImageView, + viewThemeUtils: ViewThemeUtils + ) { + val split = user.split("@") + val userId = split.getOrNull(0) ?: user + val server = split.getOrNull(1) + + val url = if (server != null) { + "https://$server/index.php/avatar/$userId/${resources.getInteger(R.integer.file_avatar_px)}" + } else { + // fallback: no federated server, maybe use local avatar + null + } + + val placeholder: Drawable = try { + TextDrawable.createAvatarByUserId(userId, avatarRadius) + } catch (e: Exception) { + Log_OC.e(TAG, "Error calculating RGB value for active account icon.", e) + viewThemeUtils.platform.colorDrawable( + ResourcesCompat + .getDrawable(resources, R.drawable.account_circle_white, null)!!, + ContextCompat.getColor(context, R.color.black) + ) + } + + avatar.tag = null + if (url != null) { + loadCircularBitmapIntoImageView(context, url, avatar, placeholder) + } else { + avatar.setImageDrawable(placeholder) + } + } + + override fun avatarGenerated(avatarDrawable: Drawable?, callContext: Any) { + (callContext as ImageView).setImageDrawable(avatarDrawable) + } + + override fun shouldCallGeneratedCallback(tag: String?, callContext: Any): Boolean = + (callContext as ImageView).tag == tag + + companion object { + private val TAG: String = AvatarGroupLayout::class.java.simpleName + private const val MAX_AVATAR_COUNT = 3 + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/CompletionCallback.kt b/app/src/main/java/com/owncloud/android/ui/CompletionCallback.kt new file mode 100644 index 0000000..3bb0a84 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/CompletionCallback.kt @@ -0,0 +1,12 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui + +interface CompletionCallback { + fun onComplete(value: Boolean) +} diff --git a/app/src/main/java/com/owncloud/android/ui/EmptyRecyclerView.java b/app/src/main/java/com/owncloud/android/ui/EmptyRecyclerView.java index 0d73e63..7c1ce64 100644 --- a/app/src/main/java/com/owncloud/android/ui/EmptyRecyclerView.java +++ b/app/src/main/java/com/owncloud/android/ui/EmptyRecyclerView.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2018 Tobias Kaminsky * SPDX-FileCopyrightText: 2018 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui; diff --git a/app/src/main/java/com/owncloud/android/ui/ListPreferenceDialog.kt b/app/src/main/java/com/owncloud/android/ui/ListPreferenceDialog.kt new file mode 100644 index 0000000..6cacb9b --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/ListPreferenceDialog.kt @@ -0,0 +1,44 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui + +import android.app.AlertDialog +import android.app.Dialog +import android.content.Context +import android.preference.ListPreference +import android.util.AttributeSet +import com.nextcloud.utils.extensions.setVisibleIf + +@Suppress("DEPRECATION") +class ListPreferenceDialog(context: Context?, attrs: AttributeSet?) : ListPreference(context, attrs) { + + fun showDialog() { + if (!isDialogCreated()) { + onClick() + } + } + + fun dismissible(value: Boolean) { + if (isDialogCreated()) { + dialog.setCancelable(value) + dialog.setCanceledOnTouchOutside(value) + } + } + + fun enableCancelButton(value: Boolean) { + if (isDialogCreated()) { + (dialog as? AlertDialog)?.let { + val cancelButton = it.getButton(Dialog.BUTTON_NEGATIVE) + cancelButton?.setVisibleIf(value) + cancelButton?.isEnabled = value + } + } + } + + private fun isDialogCreated(): Boolean = dialog != null +} diff --git a/app/src/main/java/com/owncloud/android/ui/NextcloudWebViewClient.kt b/app/src/main/java/com/owncloud/android/ui/NextcloudWebViewClient.kt index dbb7282..3e65201 100644 --- a/app/src/main/java/com/owncloud/android/ui/NextcloudWebViewClient.kt +++ b/app/src/main/java/com/owncloud/android/ui/NextcloudWebViewClient.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 Elv1zz * SPDX-FileCopyrightText: 2022 Unpublished - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui diff --git a/app/src/main/java/com/owncloud/android/ui/SquareImageView.java b/app/src/main/java/com/owncloud/android/ui/SquareImageView.java index 778ae3c..c4b2a1d 100644 --- a/app/src/main/java/com/owncloud/android/ui/SquareImageView.java +++ b/app/src/main/java/com/owncloud/android/ui/SquareImageView.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2014-2018 Tobias Kaminsky * SPDX-FileCopyrightText: 2017 Andy Scherzinger - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui; diff --git a/app/src/main/java/com/owncloud/android/ui/SquareLinearLayout.java b/app/src/main/java/com/owncloud/android/ui/SquareLinearLayout.java index 6345317..bca3b38 100644 --- a/app/src/main/java/com/owncloud/android/ui/SquareLinearLayout.java +++ b/app/src/main/java/com/owncloud/android/ui/SquareLinearLayout.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2014 Tobias Kaminsky - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui; diff --git a/app/src/main/java/com/owncloud/android/ui/StatusDrawable.java b/app/src/main/java/com/owncloud/android/ui/StatusDrawable.java index a81d7e2..129811f 100644 --- a/app/src/main/java/com/owncloud/android/ui/StatusDrawable.java +++ b/app/src/main/java/com/owncloud/android/ui/StatusDrawable.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2020 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui; @@ -35,8 +35,6 @@ public class StatusDrawable extends Drawable { private Paint backgroundPaint; private final float radius; private Context context; - private final static int whiteBackground = Color.argb(200, 255, 255, 255); - private final static int onlineStatus = Color.argb(255, 73, 179, 130); public StatusDrawable(Status status, float statusSize, Context context) { backgroundPaint = new Paint(); @@ -46,21 +44,24 @@ public class StatusDrawable extends Drawable { radius = statusSize; if (TextUtils.isEmpty(status.getIcon())) { + this.context = context; + backgroundPaint.setColor(context.getColor(R.color.bg_default)); + switch (status.getStatus()) { case DND: icon = R.drawable.ic_user_status_dnd; - backgroundPaint.setColor(whiteBackground); - this.context = context; + break; + + case BUSY: + icon = R.drawable.ic_user_status_busy; break; case ONLINE: - backgroundPaint.setColor(onlineStatus); + icon = R.drawable.ic_user_status_online; break; case AWAY: icon = R.drawable.ic_user_status_away; - backgroundPaint.setColor(whiteBackground); - this.context = context; break; default: @@ -71,7 +72,7 @@ public class StatusDrawable extends Drawable { } else { text = status.getIcon(); - backgroundPaint.setColor(whiteBackground); + backgroundPaint = null; textPaint = new Paint(); textPaint.setColor(Color.WHITE); diff --git a/app/src/main/java/com/owncloud/android/ui/TextDrawable.java b/app/src/main/java/com/owncloud/android/ui/TextDrawable.java index c017a01..10321ef 100644 --- a/app/src/main/java/com/owncloud/android/ui/TextDrawable.java +++ b/app/src/main/java/com/owncloud/android/ui/TextDrawable.java @@ -5,7 +5,7 @@ * SPDX-FileCopyrightText: 2019-2021 Chris Narkiewicz * SPDX-FileCopyrightText: 2015-2020 Tobias Kaminsky * SPDX-FileCopyrightText: 2016-2018 Andy Scherzinger - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui; diff --git a/app/src/main/java/com/owncloud/android/ui/ThemeableSwitchPreference.java b/app/src/main/java/com/owncloud/android/ui/ThemeableSwitchPreference.java index 1ddddb9..2795296 100644 --- a/app/src/main/java/com/owncloud/android/ui/ThemeableSwitchPreference.java +++ b/app/src/main/java/com/owncloud/android/ui/ThemeableSwitchPreference.java @@ -3,10 +3,11 @@ * * SPDX-FileCopyrightText: 2017 Tobias Kaminsky * SPDX-FileCopyrightText: 2017 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui; +import android.annotation.SuppressLint; import android.content.Context; import android.preference.SwitchPreference; import android.util.AttributeSet; @@ -54,9 +55,7 @@ public class ThemeableSwitchPreference extends SwitchPreference { for (int i = 0; i < viewGroup.getChildCount(); i++) { View child = viewGroup.getChildAt(i); - if (child instanceof Switch) { - Switch switchView = (Switch) child; - + if (child instanceof @SuppressLint("UseSwitchCompatOrMaterialCode") Switch switchView) { viewThemeUtils.platform.colorSwitch(switchView); break; diff --git a/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.java b/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.java index e5bef49..f61121e 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesActivity.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2018 Edvard Holst - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activities; @@ -13,6 +13,7 @@ import android.view.View; import com.google.android.material.snackbar.Snackbar; import com.nextcloud.client.network.ClientFactory; +import com.nextcloud.client.network.ConnectivityService; import com.nextcloud.common.NextcloudClient; import com.owncloud.android.R; import com.owncloud.android.databinding.ActivityListLayoutBinding; @@ -29,7 +30,6 @@ import com.owncloud.android.ui.interfaces.ActivityListInterface; import com.owncloud.android.ui.preview.PreviewImageActivity; import com.owncloud.android.ui.preview.PreviewImageFragment; import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.theme.ViewThemeUtils; import java.util.List; @@ -51,7 +51,7 @@ public class ActivitiesActivity extends DrawerActivity implements ActivityListIn ActivityListLayoutBinding binding; private ActivityListAdapter adapter; - private int lastGiven; + private long lastGiven; private boolean isLoadingActivities; private ActivitiesContract.ActionListener actionListener; private Snackbar snackbar; @@ -59,7 +59,7 @@ public class ActivitiesActivity extends DrawerActivity implements ActivityListIn @Inject ActivitiesRepository activitiesRepository; @Inject FilesRepository filesRepository; @Inject ClientFactory clientFactory; - @Inject ViewThemeUtils viewThemeUtils; + @Inject ConnectivityService connectivityService; @Override protected void onCreate(Bundle savedInstanceState) { @@ -77,7 +77,7 @@ public class ActivitiesActivity extends DrawerActivity implements ActivityListIn viewThemeUtils.androidx.themeSwipeRefreshLayout(binding.swipeContainingList); // setup drawer - setupDrawer(R.id.nav_activity); + setupDrawer(); updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_item_activities)); binding.swipeContainingList.setOnRefreshListener(() -> { @@ -153,11 +153,7 @@ public class ActivitiesActivity extends DrawerActivity implements ActivityListIn @Override protected void onResume() { super.onResume(); - actionListener.onResume(); - - setDrawerMenuItemChecked(R.id.nav_activity); - setupContent(); } @@ -168,11 +164,8 @@ public class ActivitiesActivity extends DrawerActivity implements ActivityListIn } @Override - public void showActivities(List activities, NextcloudClient client, int lastGiven) { - boolean clear = false; - if (this.lastGiven == ActivitiesContract.ActionListener.UNDEFINED) { - clear = true; - } + public void showActivities(List activities, NextcloudClient client, long lastGiven) { + boolean clear = this.lastGiven == ActivitiesContract.ActionListener.UNDEFINED; adapter.setActivityItems(activities, client, clear); this.lastGiven = lastGiven; @@ -190,7 +183,16 @@ public class ActivitiesActivity extends DrawerActivity implements ActivityListIn @Override public void showActivitiesLoadError(String error) { - snackbar = DisplayUtils.showSnackMessage(this, error); + connectivityService.isNetworkAndServerAvailable(result -> { + if (result) { + snackbar = DisplayUtils.showSnackMessage(this, error); + } else { + showEmptyContent(getString(R.string.server_not_reachable), + getString(R.string.server_not_reachable_content)); + binding.emptyList.emptyListIcon.setImageResource(R.drawable.ic_sync_off); + } + }); + } @Override diff --git a/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesContract.java b/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesContract.java index 4a0bd19..b5396d4 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesContract.java +++ b/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesContract.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2018 Edvard Holst - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activities; @@ -15,7 +15,7 @@ import java.util.List; public interface ActivitiesContract { interface View { - void showActivities(List activities, NextcloudClient client, int lastGiven); + void showActivities(List activities, NextcloudClient client, long lastGiven); void showActivitiesLoadError(String error); void showActivityDetailUI(OCFile ocFile); void showActivityDetailUIIsNull(); @@ -28,7 +28,7 @@ public interface ActivitiesContract { interface ActionListener { int UNDEFINED = -1; - void loadActivities(int lastGiven); + void loadActivities(long lastGiven); void openActivity(String fileUrl, BaseActivity baseActivity); diff --git a/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesPresenter.java b/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesPresenter.java index e39ec74..1c4ff09 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesPresenter.java +++ b/app/src/main/java/com/owncloud/android/ui/activities/ActivitiesPresenter.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2018 Edvard Holst - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activities; @@ -34,7 +34,7 @@ public class ActivitiesPresenter implements ActivitiesContract.ActionListener { } @Override - public void loadActivities(int lastGiven) { + public void loadActivities(long lastGiven) { if (UNDEFINED == lastGiven) { activitiesView.showLoadingMessage(); } else { @@ -42,7 +42,7 @@ public class ActivitiesPresenter implements ActivitiesContract.ActionListener { } activitiesRepository.getActivities(lastGiven, new ActivitiesRepository.LoadActivitiesCallback() { @Override - public void onActivitiesLoaded(List activities, NextcloudClient client, int lastGiven) { + public void onActivitiesLoaded(List activities, NextcloudClient client, long lastGiven) { if (!activityStopped) { activitiesView.setProgressIndicatorState(false); diff --git a/app/src/main/java/com/owncloud/android/ui/activities/StickyHeaderItemDecoration.java b/app/src/main/java/com/owncloud/android/ui/activities/StickyHeaderItemDecoration.java index ee6ac9d..f862db5 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/StickyHeaderItemDecoration.java +++ b/app/src/main/java/com/owncloud/android/ui/activities/StickyHeaderItemDecoration.java @@ -5,7 +5,7 @@ * Copyright (C) 2019 Sevastyan Savanyuk * Copyright (C) 2019 Nextcloud GmbH * - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activities; diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesRepository.java b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesRepository.java index 688a90d..e23c3a3 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesRepository.java +++ b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesRepository.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2018 Edvard Holst - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activities.data.activities; @@ -17,9 +17,9 @@ import androidx.annotation.NonNull; */ public interface ActivitiesRepository { interface LoadActivitiesCallback { - void onActivitiesLoaded(List activities, NextcloudClient client, int lastGiven); + void onActivitiesLoaded(List activities, NextcloudClient client, long lastGiven); void onActivitiesLoadedError(String error); } - void getActivities(int lastGiven, @NonNull LoadActivitiesCallback callback); + void getActivities(long lastGiven, @NonNull LoadActivitiesCallback callback); } diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApi.java b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApi.java index 1b561b3..870416c 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApi.java +++ b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApi.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2018 Edvard Holst - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activities.data.activities; @@ -19,10 +19,10 @@ import java.util.List; public interface ActivitiesServiceApi { interface ActivitiesServiceCallback { - void onLoaded(T activities, NextcloudClient client, int lastGiven); + void onLoaded(T activities, NextcloudClient client, long lastGiven); void onError (String error); } - void getAllActivities(int lastGiven, ActivitiesServiceApi.ActivitiesServiceCallback> callback); + void getAllActivities(long lastGiven, ActivitiesServiceApi.ActivitiesServiceCallback> callback); } diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApiImpl.java b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApiImpl.java index 4e81beb..0534b6f 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApiImpl.java +++ b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivitiesServiceApiImpl.java @@ -1,9 +1,11 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2019 Chris Narkiewicz * SPDX-FileCopyrightText: 2018 Edvard Holst - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 TSI-mc + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activities.data.activities; @@ -42,7 +44,7 @@ public class ActivitiesServiceApiImpl implements ActivitiesServiceApi { } @Override - public void getAllActivities(int lastGiven, ActivitiesServiceCallback> callback) { + public void getAllActivities(long lastGiven, ActivitiesServiceCallback> callback) { GetActivityListTask getActivityListTask = new GetActivityListTask(accountManager.getUser(), lastGiven, callback); @@ -54,12 +56,12 @@ public class ActivitiesServiceApiImpl implements ActivitiesServiceApi { private final ActivitiesServiceCallback> callback; private List activities; private final User user; - private int lastGiven; + private long lastGiven; private String errorMessage; private NextcloudClient client; private GetActivityListTask(User user, - int lastGiven, + long lastGiven, ActivitiesServiceCallback> callback) { this.user = user; this.lastGiven = lastGiven; @@ -89,12 +91,12 @@ public class ActivitiesServiceApiImpl implements ActivitiesServiceApi { final ArrayList data = result.getData(); activities = (ArrayList) data.get(0); - lastGiven = (int) data.get(1); + lastGiven = (long) data.get(1); return Boolean.TRUE; } else { Log_OC.d(TAG, result.getLogMessage()); // show error - errorMessage = result.getLogMessage(); + errorMessage = result.getLogMessage(MainApp.getAppContext()); if (result.getHttpCode() == HttpStatus.SC_NOT_MODIFIED) { errorMessage = context.getString(R.string.file_list_empty_headline_server_search); } diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivityRepositories.java b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivityRepositories.java index b393f66..b4bfeb5 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivityRepositories.java +++ b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/ActivityRepositories.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2018 Edvard Holst - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activities.data.activities; diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/RemoteActivitiesRepository.java b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/RemoteActivitiesRepository.java index 8812a6f..82adde6 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/data/activities/RemoteActivitiesRepository.java +++ b/app/src/main/java/com/owncloud/android/ui/activities/data/activities/RemoteActivitiesRepository.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2018 Edvard Holst - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activities.data.activities; @@ -22,18 +22,18 @@ public class RemoteActivitiesRepository implements ActivitiesRepository { @Override - public void getActivities(int lastGiven, @NonNull LoadActivitiesCallback callback) { + public void getActivities(long lastGiven, @NonNull LoadActivitiesCallback callback) { activitiesServiceApi.getAllActivities(lastGiven, - new ActivitiesServiceApi.ActivitiesServiceCallback>() { - @Override - public void onLoaded(List activities, NextcloudClient client, int lastGiven) { - callback.onActivitiesLoaded(activities, client, lastGiven); - } + new ActivitiesServiceApi.ActivitiesServiceCallback<>() { + @Override + public void onLoaded(List activities, NextcloudClient client, long lastGiven) { + callback.onActivitiesLoaded(activities, client, lastGiven); + } - @Override - public void onError(String error) { - callback.onActivitiesLoadedError(error); - } - }); + @Override + public void onError(String error) { + callback.onActivitiesLoadedError(error); + } + }); } } diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/files/FileRepositories.java b/app/src/main/java/com/owncloud/android/ui/activities/data/files/FileRepositories.java index 4f90cbe..37da9e9 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/data/files/FileRepositories.java +++ b/app/src/main/java/com/owncloud/android/ui/activities/data/files/FileRepositories.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2018 Edvard Holst - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activities.data.files; diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesRepository.java b/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesRepository.java index beaed7b..49300a4 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesRepository.java +++ b/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesRepository.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2018 Edvard Holst - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activities.data.files; diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApi.java b/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApi.java index e06a3b7..5d08e56 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApi.java +++ b/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApi.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2018 Edvard Holst - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activities.data.files; diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApiImpl.java b/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApiImpl.java index b569bf8..8ad1408 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApiImpl.java +++ b/app/src/main/java/com/owncloud/android/ui/activities/data/files/FilesServiceApiImpl.java @@ -3,10 +3,11 @@ * * SPDX-FileCopyrightText: 2019 Chris Narkiewicz * SPDX-FileCopyrightText: 2018 Edvard Holst - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activities.data.files; +import android.annotation.SuppressLint; import android.content.Context; import android.os.AsyncTask; @@ -33,8 +34,8 @@ public class FilesServiceApiImpl implements FilesServiceApi { private static final String TAG = FilesServiceApiImpl.class.getSimpleName(); - private UserAccountManager accountManager; - private ClientFactory clientFactory; + private final UserAccountManager accountManager; + private final ClientFactory clientFactory; public FilesServiceApiImpl(UserAccountManager accountManager, ClientFactory clientFactory) { this.accountManager = accountManager; @@ -58,7 +59,7 @@ public class FilesServiceApiImpl implements FilesServiceApi { private OCFile remoteOcFile; private String errorMessage; // TODO: Figure out a better way to do this than passing a BaseActivity reference. - private final BaseActivity baseActivity; + @SuppressLint("StaticFieldLeak") private final BaseActivity baseActivity; private final String fileUrl; private final User user; private final UserAccountManager accountManager; diff --git a/app/src/main/java/com/owncloud/android/ui/activities/data/files/RemoteFilesRepository.java b/app/src/main/java/com/owncloud/android/ui/activities/data/files/RemoteFilesRepository.java index 1ec23f6..759e0b4 100644 --- a/app/src/main/java/com/owncloud/android/ui/activities/data/files/RemoteFilesRepository.java +++ b/app/src/main/java/com/owncloud/android/ui/activities/data/files/RemoteFilesRepository.java @@ -2,7 +2,7 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2018 Edvard Holst - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activities.data.files; @@ -22,16 +22,16 @@ public class RemoteFilesRepository implements FilesRepository { @Override public void readRemoteFile(String path, BaseActivity activity, @NonNull ReadRemoteFileCallback callback) { - filesServiceApi.readRemoteFile(path, activity, new FilesServiceApi.FilesServiceCallback() { - @Override - public void onLoaded(OCFile ocFile) { - callback.onFileLoaded(ocFile); - } + filesServiceApi.readRemoteFile(path, activity, new FilesServiceApi.FilesServiceCallback<>() { + @Override + public void onLoaded(OCFile ocFile) { + callback.onFileLoaded(ocFile); + } - @Override - public void onError(String error) { - callback.onFileLoadError(error); - } - }); + @Override + public void onError(String error) { + callback.onFileLoadError(error); + } + }); } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/BaseActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/BaseActivity.java index 7a57b67..82a0598 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/BaseActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/BaseActivity.java @@ -2,7 +2,8 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2024 TSI-mc + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activity; @@ -10,6 +11,7 @@ import android.accounts.Account; import android.content.Intent; import android.os.Bundle; +import com.nextcloud.android.common.ui.util.extensions.AppCompatActivityExtensionsKt; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.di.Injectable; @@ -17,6 +19,8 @@ import com.nextcloud.client.mixins.MixinRegistry; import com.nextcloud.client.mixins.SessionMixin; import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.client.preferences.DarkMode; +import com.nextcloud.repository.ClientRepository; +import com.nextcloud.repository.RemoteClientRepository; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.lib.common.utils.Log_OC; @@ -43,14 +47,14 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab private boolean paused; protected boolean enableAccountHandling = true; - private MixinRegistry mixinRegistry = new MixinRegistry(); + private final MixinRegistry mixinRegistry = new MixinRegistry(); private SessionMixin sessionMixin; @Inject UserAccountManager accountManager; @Inject AppPreferences preferences; @Inject FileDataStorageManager fileDataStorageManager; - private AppPreferences.Listener onPreferencesChanged = new AppPreferences.Listener() { + private final AppPreferences.Listener onPreferencesChanged = new AppPreferences.Listener() { @Override public void onDarkThemeModeChanged(DarkMode mode) { onThemeSettingsModeChanged(); @@ -61,8 +65,11 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab return accountManager; } + private ClientRepository clientRepository; + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { + AppCompatActivityExtensionsKt.applyEdgeToEdgeWithSystemBarPadding(this); super.onCreate(savedInstanceState); sessionMixin = new SessionMixin(this, accountManager); mixinRegistry.add(sessionMixin); @@ -70,6 +77,8 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab if (enableAccountHandling) { mixinRegistry.onCreate(savedInstanceState); } + + clientRepository = new RemoteClientRepository(accountManager.getUser(), this, this); } @Override @@ -115,7 +124,9 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab protected void onRestart() { Log_OC.v(TAG, "onRestart() start"); super.onRestart(); - mixinRegistry.onRestart(); + if (enableAccountHandling) { + mixinRegistry.onRestart(); + } } private void onThemeSettingsModeChanged() { @@ -175,4 +186,9 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab public FileDataStorageManager getStorageManager() { return fileDataStorageManager; } + + public ClientRepository getClientRepository() { + return clientRepository; + } + } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ChooseStorageLocationActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/ChooseStorageLocationActivity.kt new file mode 100644 index 0000000..8d13d53 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/ChooseStorageLocationActivity.kt @@ -0,0 +1,38 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 ZetaTom <70907959+ZetaTom@users.noreply.github.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.activity + +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.nextcloud.ui.ChooseStorageLocationDialogFragment + +class ChooseStorageLocationActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val chooseStorageLocationDialogFragment = ChooseStorageLocationDialogFragment.newInstance() + supportFragmentManager.setFragmentResultListener( + KEY_RESULT_STORAGE_LOCATION, + this + ) { _, result -> + setResult( + ChooseStorageLocationDialogFragment.STORAGE_LOCATION_RESULT_CODE, + Intent().putExtra( + KEY_RESULT_STORAGE_LOCATION, + result.getString(KEY_RESULT_STORAGE_LOCATION) + ) + ) + } + chooseStorageLocationDialogFragment.show(supportFragmentManager, "choose_storage_location") + } + + companion object { + const val KEY_RESULT_STORAGE_LOCATION = ChooseStorageLocationDialogFragment.KEY_RESULT_STORAGE_LOCATION + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/CommunityActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/CommunityActivity.kt index 78eafe2..308d7eb 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/CommunityActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/CommunityActivity.kt @@ -1,11 +1,11 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2016 Andy Scherzinger * SPDX-FileCopyrightText: 2016 Tobias Kaminsky * SPDX-FileCopyrightText: 2016 Nextcloud - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activity @@ -30,7 +30,7 @@ open class CommunityActivity : DrawerActivity() { setupToolbar() updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_community)) - setupDrawer(R.id.nav_community) + setupDrawer() binding.communityReleaseCandidateText.movementMethod = LinkMovementMethod.getInstance() setupContributeForumView() setupContributeTranslationView() @@ -125,9 +125,4 @@ open class CommunityActivity : DrawerActivity() { } return retval } - - override fun onResume() { - super.onResume() - setDrawerMenuItemChecked(R.id.nav_community) - } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ComponentsGetter.java b/app/src/main/java/com/owncloud/android/ui/activity/ComponentsGetter.java index 05ebeae..7ce91df 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ComponentsGetter.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ComponentsGetter.java @@ -1,12 +1,12 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2016 ownCloud Inc. * SPDX-FileCopyrightText: 2015 María Asensio Valverde * SPDX-FileCopyrightText: 2014 David A. Velasco * SPDX-FileCopyrightText: 2012 Bartosz Przybylski - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.ui.activity; diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt index a83e5c6..fc5f579 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt @@ -2,27 +2,36 @@ * Nextcloud - Android Client * * SPDX-FileCopyrightText: 2024 Jonas Mayer - * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-FileCopyrightText: 2019 Alice Gaudon * SPDX-FileCopyrightText: 2012 Bartosz Przybylski - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.ui.activity +import android.annotation.SuppressLint +import android.app.NotificationManager import android.content.Context import android.content.Intent import android.os.Bundle import android.widget.Toast +import androidx.fragment.app.FragmentTransaction +import androidx.lifecycle.lifecycleScope import com.nextcloud.client.account.User +import com.nextcloud.client.database.entity.OfflineOperationEntity import com.nextcloud.client.jobs.download.FileDownloadHelper +import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsNotificationManager +import com.nextcloud.client.jobs.operation.FileOperationHelper import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.jobs.upload.FileUploadWorker import com.nextcloud.client.jobs.upload.UploadNotificationManager import com.nextcloud.model.HTTPStatusCodes +import com.nextcloud.utils.extensions.getDecryptedPath import com.nextcloud.utils.extensions.getParcelableArgument +import com.nextcloud.utils.extensions.logFileSize import com.owncloud.android.R -import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.db.OCUpload import com.owncloud.android.files.services.NameCollisionPolicy @@ -32,25 +41,32 @@ import com.owncloud.android.lib.resources.files.model.RemoteFile import com.owncloud.android.ui.dialog.ConflictsResolveDialog import com.owncloud.android.ui.dialog.ConflictsResolveDialog.Decision import com.owncloud.android.ui.dialog.ConflictsResolveDialog.OnConflictDecisionMadeListener +import com.owncloud.android.ui.notifications.NotificationUtils import com.owncloud.android.utils.FileStorageUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject /** * Wrapper activity which will be launched if keep-in-sync file will be modified by external application. */ -class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener { - @JvmField +@Suppress("TooManyFunctions") +class ConflictsResolveActivity : + FileActivity(), + OnConflictDecisionMadeListener { @Inject - var uploadsStorageManager: UploadsStorageManager? = null + lateinit var uploadsStorageManager: UploadsStorageManager - @JvmField @Inject - var fileStorageManager: FileDataStorageManager? = null + lateinit var fileOperationHelper: FileOperationHelper private var conflictUploadId: Long = 0 + private var offlineOperationPath: String? = null private var existingFile: OCFile? = null private var newFile: OCFile? = null private var localBehaviour = FileUploadWorker.LOCAL_BEHAVIOUR_FORGET + private lateinit var offlineOperationNotificationManager: OfflineOperationsNotificationManager @JvmField var listener: OnConflictDecisionMadeListener? = null @@ -60,7 +76,7 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener getArguments(savedInstanceState) - val upload = uploadsStorageManager?.getUploadById(conflictUploadId) + val upload = uploadsStorageManager.getUploadById(conflictUploadId) if (upload != null) { localBehaviour = upload.localAction } @@ -68,6 +84,7 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener // new file was modified locally in file system newFile = file setupOnConflictDecisionMadeListener(upload) + offlineOperationNotificationManager = OfflineOperationsNotificationManager(this, viewThemeUtils) } private fun getArguments(savedInstanceState: Bundle?) { @@ -75,7 +92,9 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener conflictUploadId = savedInstanceState.getLong(EXTRA_CONFLICT_UPLOAD_ID) existingFile = savedInstanceState.getParcelableArgument(EXTRA_EXISTING_FILE, OCFile::class.java) localBehaviour = savedInstanceState.getInt(EXTRA_LOCAL_BEHAVIOUR) + offlineOperationPath = savedInstanceState.getString(EXTRA_OFFLINE_OPERATION_PATH) } else { + offlineOperationPath = intent.getStringExtra(EXTRA_OFFLINE_OPERATION_PATH) conflictUploadId = intent.getLongExtra(EXTRA_CONFLICT_UPLOAD_ID, -1) existingFile = intent.getParcelableArgument(EXTRA_EXISTING_FILE, OCFile::class.java) localBehaviour = intent.getIntExtra(EXTRA_LOCAL_BEHAVIOUR, localBehaviour) @@ -84,93 +103,204 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener private fun setupOnConflictDecisionMadeListener(upload: OCUpload?) { listener = OnConflictDecisionMadeListener { decision: Decision? -> - val file = newFile // local file got changed, so either upload it or replace it again by server + + // local file got changed, so either upload it or replace it again by server + val file = newFile + // version val user = user.orElseThrow { RuntimeException() } - when (decision) { - Decision.CANCEL -> {} - Decision.KEEP_LOCAL -> { - upload?.let { - FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName) - } - FileUploadHelper.instance().uploadUpdatedFile( - user, - arrayOf(file), - localBehaviour, - NameCollisionPolicy.OVERWRITE - ) - } - Decision.KEEP_BOTH -> { - upload?.let { - FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName) - } - FileUploadHelper.instance().uploadUpdatedFile( - user, - arrayOf(file), - localBehaviour, - NameCollisionPolicy.RENAME - ) - } - - Decision.KEEP_SERVER -> { - if (!shouldDeleteLocal()) { - // Overwrite local file - file?.let { - FileDownloadHelper.instance().downloadFile( - getUser().orElseThrow { RuntimeException() }, - file, - conflictUploadId = conflictUploadId - ) - } - } - - upload?.let { - FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName) - - UploadNotificationManager( - applicationContext, - viewThemeUtils - ).dismissOldErrorNotification(it.remotePath, it.localPath) - } - } - - else -> {} + val offlineOperation = if (offlineOperationPath != null) { + fileDataStorageManager.offlineOperationDao.getByPath(offlineOperationPath!!) + } else { + null } + + when (decision) { + Decision.KEEP_LOCAL -> keepLocal(file, upload, user) + Decision.KEEP_BOTH -> keepBoth(file, upload, user) + Decision.KEEP_SERVER -> keepServer(file, upload) + Decision.KEEP_OFFLINE_FOLDER -> keepOfflineFolder(file, offlineOperation) + Decision.KEEP_SERVER_FOLDER -> keepServerFile(offlineOperation) + Decision.KEEP_BOTH_FOLDER -> keepBothFolder(offlineOperation, file) + else -> Unit + } + + upload?.remotePath?.let { oldFilePath -> + val oldFile = storageManager.getFileByDecryptedRemotePath(oldFilePath) + updateThumbnailIfNeeded(decision, file, oldFile) + } + + dismissConflictResolveNotification(file) finish() } } + private fun updateThumbnailIfNeeded(decision: Decision?, file: OCFile?, oldFile: OCFile?) { + if (decision == Decision.KEEP_BOTH || decision == Decision.KEEP_LOCAL) { + // When the user chooses to replace the remote file with the new local file, + // remove the old file's thumbnail so a new one can be generated + if (decision == Decision.KEEP_LOCAL) { + ThumbnailsCacheManager.removeFromCache(oldFile) + } + + file?.isUpdateThumbnailNeeded = true + fileDataStorageManager.saveFile(file) + } + } + + private fun dismissConflictResolveNotification(file: OCFile?) { + file ?: return + + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + val tag = NotificationUtils.createUploadNotificationTag(file) + notificationManager.cancel(tag, FileUploadWorker.NOTIFICATION_ERROR_ID) + } + + private fun keepBothFolder(offlineOperation: OfflineOperationEntity?, serverFile: OCFile?) { + offlineOperation ?: return + fileDataStorageManager.keepOfflineOperationAndServerFile(offlineOperation, serverFile) + backgroundJobManager.startOfflineOperations() + offlineOperationNotificationManager.dismissNotification(offlineOperation.id) + } + + private fun keepServerFile(offlineOperation: OfflineOperationEntity?) { + offlineOperation ?: return + fileDataStorageManager.offlineOperationDao.delete(offlineOperation) + + val id = offlineOperation.id ?: return + offlineOperationNotificationManager.dismissNotification(id) + } + + private fun keepOfflineFolder(serverFile: OCFile?, offlineOperation: OfflineOperationEntity?) { + serverFile ?: return + offlineOperation ?: return + + lifecycleScope.launch(Dispatchers.IO) { + val client = clientRepository.getOwncloudClient() ?: return@launch + val isSuccess = fileOperationHelper.removeFile( + serverFile, + onlyLocalCopy = false, + inBackground = false, + client = client + ) + + if (isSuccess) { + backgroundJobManager.startOfflineOperations() + withContext(Dispatchers.Main) { + offlineOperationNotificationManager.dismissNotification(offlineOperation.id) + } + } + } + } + + private fun keepLocal(file: OCFile?, upload: OCUpload?, user: User) { + upload?.let { + FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName) + } + + FileUploadHelper.instance().uploadUpdatedFile( + user, + arrayOf(file), + localBehaviour, + NameCollisionPolicy.OVERWRITE + ) + } + + private fun keepBoth(file: OCFile?, upload: OCUpload?, user: User) { + upload?.let { + FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName) + } + + FileUploadHelper.instance().uploadUpdatedFile( + user, + arrayOf(file), + localBehaviour, + NameCollisionPolicy.RENAME + ) + } + + private fun keepServer(file: OCFile?, upload: OCUpload?) { + if (!shouldDeleteLocal()) { + // Overwrite local file + file?.let { + FileDownloadHelper.instance().downloadFile( + user.orElseThrow { RuntimeException() }, + file, + conflictUploadId = conflictUploadId + ) + } + } + + upload?.let { + FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName) + + UploadNotificationManager( + applicationContext, + viewThemeUtils, + upload.uploadId.toInt() + ).dismissOldErrorNotification(it.remotePath, it.localPath) + } + } + override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putLong(EXTRA_CONFLICT_UPLOAD_ID, conflictUploadId) - outState.putParcelable(EXTRA_EXISTING_FILE, existingFile) - outState.putInt(EXTRA_LOCAL_BEHAVIOUR, localBehaviour) + existingFile.logFileSize(TAG) + + outState.run { + putLong(EXTRA_CONFLICT_UPLOAD_ID, conflictUploadId) + putParcelable(EXTRA_EXISTING_FILE, existingFile) + putInt(EXTRA_LOCAL_BEHAVIOUR, localBehaviour) + } } - override fun conflictDecisionMade(decision: Decision) { + override fun conflictDecisionMade(decision: Decision?) { listener?.conflictDecisionMade(decision) } + @Suppress("ReturnCount") override fun onStart() { super.onStart() + if (account == null) { finish() return } + if (newFile == null) { Log_OC.e(TAG, "No file received") finish() return } + + offlineOperationPath?.let { path -> + newFile?.let { ocFile -> + val offlineOperation = fileDataStorageManager.offlineOperationDao.getByPath(path) + + if (offlineOperation == null) { + showErrorAndFinish() + return + } + + val (ft, _) = prepareDialog() + val dialog = ConflictsResolveDialog.newInstance( + context = this, + leftFile = offlineOperation, + rightFile = ocFile + ) + dialog.show(ft, "conflictDialog") + return + } + } + if (existingFile == null) { - val remotePath = fileStorageManager?.retrieveRemotePathConsideringEncryption(newFile) ?: return + val remotePath = fileDataStorageManager.retrieveRemotePathConsideringEncryption(newFile) ?: return val operation = ReadFileRemoteOperation(remotePath) @Suppress("TooGenericExceptionCaught") - Thread { + lifecycleScope.launch(Dispatchers.IO) { try { - val result = operation.execute(account, this) + val result = operation.execute(account, this@ConflictsResolveActivity) if (result.isSuccess) { existingFile = FileStorageUtils.fillOCFile(result.data[0] as RemoteFile) existingFile?.lastSyncDateForProperties = System.currentTimeMillis() @@ -183,14 +313,15 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener Log_OC.e(TAG, "Error when trying to fetch remote file", e) showErrorAndFinish() } - }.start() + } } else { - val remotePath = fileStorageManager?.retrieveRemotePathConsideringEncryption(existingFile) ?: return + val remotePath = fileDataStorageManager.retrieveRemotePathConsideringEncryption(existingFile) ?: return startDialog(remotePath) } } - private fun startDialog(remotePath: String) { + @SuppressLint("CommitTransaction") + private fun prepareDialog(): Pair { val userOptional = user if (!userOptional.isPresent) { Log_OC.e(TAG, "User not present") @@ -203,13 +334,22 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener if (prev != null) { fragmentTransaction.remove(prev) } - if (existingFile != null && storageManager.fileExists(remotePath)) { + + return fragmentTransaction to user.get() + } + + private fun startDialog(remotePath: String) { + val (ft, user) = prepareDialog() + + if (existingFile != null && storageManager.fileExists(remotePath) && newFile != null) { val dialog = ConflictsResolveDialog.newInstance( - existingFile, - newFile, - userOptional.get() + title = storageManager.getDecryptedPath(existingFile!!), + context = this, + leftFile = newFile!!, + rightFile = existingFile!!, + user = user ) - dialog.show(fragmentTransaction, "conflictDialog") + dialog.show(ft, "conflictDialog") } else { // Account was changed to a different one - just finish Log_OC.e(TAG, "Account was changed, finishing") @@ -219,26 +359,22 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener private fun showErrorAndFinish(code: Int? = null) { val message = parseErrorMessage(code) - runOnUiThread { - Toast.makeText(this, message, Toast.LENGTH_LONG).show() + lifecycleScope.launch(Dispatchers.Main) { + Toast.makeText(this@ConflictsResolveActivity, message, Toast.LENGTH_LONG).show() finish() } } - private fun parseErrorMessage(code: Int?): String { - return if (code == HTTPStatusCodes.NOT_FOUND.code) { - getString(R.string.uploader_file_not_found_on_server_message) - } else { - getString(R.string.conflict_dialog_error) - } + private fun parseErrorMessage(code: Int?): String = if (code == HTTPStatusCodes.NOT_FOUND.code) { + getString(R.string.uploader_file_not_found_on_server_message) + } else { + getString(R.string.conflict_dialog_error) } /** * @return whether the local version of the files is to be deleted. */ - private fun shouldDeleteLocal(): Boolean { - return localBehaviour == FileUploadWorker.LOCAL_BEHAVIOUR_DELETE - } + private fun shouldDeleteLocal(): Boolean = localBehaviour == FileUploadWorker.LOCAL_BEHAVIOUR_DELETE companion object { /** @@ -251,24 +387,26 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener */ const val EXTRA_LOCAL_BEHAVIOUR = "LOCAL_BEHAVIOUR" const val EXTRA_EXISTING_FILE = "EXISTING_FILE" + private const val EXTRA_OFFLINE_OPERATION_PATH = "EXTRA_OFFLINE_OPERATION_PATH" + private val TAG = ConflictsResolveActivity::class.java.simpleName @JvmStatic - fun createIntent( - file: OCFile?, - user: User?, - conflictUploadId: Long, - flag: Int?, - context: Context? - ): Intent { - val intent = Intent(context, ConflictsResolveActivity::class.java) - if (flag != null) { - intent.flags = intent.flags or flag + fun createIntent(file: OCFile?, user: User?, conflictUploadId: Long, flag: Int?, context: Context?): Intent = + Intent(context, ConflictsResolveActivity::class.java).apply { + if (flag != null) { + flags = flags or flag + } + putExtra(EXTRA_FILE, file) + putExtra(EXTRA_USER, user) + putExtra(EXTRA_CONFLICT_UPLOAD_ID, conflictUploadId) + } + + @JvmStatic + fun createIntent(file: OCFile, offlineOperationPath: String, context: Context): Intent = + Intent(context, ConflictsResolveActivity::class.java).apply { + putExtra(EXTRA_FILE, file) + putExtra(EXTRA_OFFLINE_OPERATION_PATH, offlineOperationPath) } - intent.putExtra(EXTRA_FILE, file) - intent.putExtra(EXTRA_USER, user) - intent.putExtra(EXTRA_CONFLICT_UPLOAD_ID, conflictUploadId) - return intent - } } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ContactsPreferenceActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ContactsPreferenceActivity.java index 42fc3fc..0811c5d 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ContactsPreferenceActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ContactsPreferenceActivity.java @@ -4,7 +4,7 @@ * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2017 Tobias Kaminsky * SPDX-FileCopyrightText: 2017 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activity; diff --git a/app/src/main/java/com/owncloud/android/ui/activity/CopyToClipboardActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/CopyToClipboardActivity.kt index 2fdd965..2811b20 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/CopyToClipboardActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/CopyToClipboardActivity.kt @@ -1,8 +1,8 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2023 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-FileCopyrightText: 2023 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activity diff --git a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java index 13d6230..2b0d1c0 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java @@ -1,14 +1,14 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2021 TSI-mc + * SPDX-FileCopyrightText: 2021-2024 TSI-mc * SPDX-FileCopyrightText: 2020 Infomaniak Network SA * SPDX-FileCopyrightText: 2020 Chris Narkiewicz * SPDX-FileCopyrightText: 2017 Tobias Kaminsky * SPDX-FileCopyrightText: 2016 Andy Scherzinger * SPDX-FileCopyrightText: 2016 Nextcloud * SPDX-FileCopyrightText: 2016 ownCloud Inc. - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.ui.activity; @@ -20,15 +20,18 @@ import android.content.Intent; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.graphics.Bitmap; +import android.graphics.Canvas; import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.LayerDrawable; +import android.graphics.drawable.PictureDrawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.os.PersistableBundle; import android.os.SystemClock; import android.text.TextUtils; import android.view.Menu; @@ -39,18 +42,17 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; -import com.bumptech.glide.GenericRequestBuilder; -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.load.model.StreamEncoder; -import com.bumptech.glide.load.resource.file.FileToStreamDecoder; -import com.bumptech.glide.request.animation.GlideAnimation; -import com.bumptech.glide.request.target.SimpleTarget; +import com.bumptech.glide.request.target.CustomTarget; +import com.bumptech.glide.request.target.Target; +import com.bumptech.glide.request.transition.Transition; +import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.button.MaterialButton; import com.google.android.material.navigation.NavigationView; import com.google.android.material.progressindicator.LinearProgressIndicator; import com.nextcloud.client.account.User; import com.nextcloud.client.di.Injectable; +import com.nextcloud.client.files.DeepLinkConstants; +import com.nextcloud.client.jobs.upload.FileUploadWorker; import com.nextcloud.client.network.ClientFactory; import com.nextcloud.client.onboarding.FirstRunActivity; import com.nextcloud.client.preferences.AppPreferences; @@ -58,6 +60,11 @@ import com.nextcloud.common.NextcloudClient; import com.nextcloud.ui.ChooseAccountDialogFragment; import com.nextcloud.ui.composeActivity.ComposeActivity; import com.nextcloud.ui.composeActivity.ComposeDestination; +import com.nextcloud.utils.GlideHelper; +import com.nextcloud.utils.LinkHelper; +import com.nextcloud.utils.extensions.ActivityExtensionsKt; +import com.nextcloud.utils.extensions.ViewExtensionsKt; +import com.nextcloud.utils.mdm.MDMConfig; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.authentication.PassCodeManager; @@ -92,12 +99,9 @@ import com.owncloud.android.ui.preview.PreviewTextStringFragment; import com.owncloud.android.ui.trashbin.TrashbinActivity; import com.owncloud.android.utils.BitmapUtils; import com.owncloud.android.utils.DisplayUtils; +import com.owncloud.android.utils.DrawableUtil; import com.owncloud.android.utils.DrawerMenuUtil; import com.owncloud.android.utils.FilesSyncHelper; -import com.owncloud.android.utils.svg.MenuSimpleTarget; -import com.owncloud.android.utils.svg.SVGorImage; -import com.owncloud.android.utils.svg.SvgOrImageBitmapTranscoder; -import com.owncloud.android.utils.svg.SvgOrImageDecoder; import com.owncloud.android.utils.theme.CapabilityUtils; import org.greenrobot.eventbus.EventBus; @@ -105,7 +109,6 @@ import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -113,14 +116,20 @@ import java.util.Optional; import javax.inject.Inject; +import androidx.activity.OnBackPressedCallback; +import androidx.annotation.IdRes; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hct.Hct; +import kotlin.Unit; /** * Base class to handle setup of the drawer implementation including user switching and avatar fetching and fallback @@ -131,7 +140,6 @@ public abstract class DrawerActivity extends ToolbarActivity private static final String TAG = DrawerActivity.class.getSimpleName(); private static final String KEY_IS_ACCOUNT_CHOOSER_ACTIVE = "IS_ACCOUNT_CHOOSER_ACTIVE"; - private static final String KEY_CHECKED_MENU_ITEM = "CHECKED_MENU_ITEM"; private static final int ACTION_MANAGE_ACCOUNTS = 101; private static final int MENU_ORDER_EXTERNAL_LINKS = 3; private static final int MENU_ITEM_EXTERNAL_LINK = 111; @@ -151,7 +159,7 @@ public abstract class DrawerActivity extends ToolbarActivity /** * Reference to the navigation view. */ - private NavigationView mNavigationView; + private NavigationView drawerNavigationView; /** * Reference to the navigation view header. @@ -166,7 +174,7 @@ public abstract class DrawerActivity extends ToolbarActivity /** * Id of the checked menu item. */ - private int mCheckedMenuItem = Menu.NONE; + public static int menuItemId = Menu.NONE; /** * container layout of the quota view. @@ -192,21 +200,18 @@ public abstract class DrawerActivity extends ToolbarActivity private ExternalLinksProvider externalLinksProvider; private ArbitraryDataProvider arbitraryDataProvider; + private BottomNavigationView bottomNavigationView; + @Inject AppPreferences preferences; @Inject ClientFactory clientFactory; - /** - * Initializes the drawer, its content and highlights the menu item with the given id. This method needs to be - * called after the content view has been set. - * - * @param menuItemId the menu item to be checked/highlighted - */ - protected void setupDrawer(int menuItemId) { - setupDrawer(); - setDrawerMenuItemChecked(menuItemId); + @Override + public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) { + super.onCreate(savedInstanceState, persistentState); + addOnBackPressedCallback(); } /** @@ -215,14 +220,14 @@ public abstract class DrawerActivity extends ToolbarActivity protected void setupDrawer() { mDrawerLayout = findViewById(R.id.drawer_layout); - mNavigationView = findViewById(R.id.nav_view); - if (mNavigationView != null) { + drawerNavigationView = findViewById(R.id.nav_view); + if (drawerNavigationView != null) { // Setting up drawer header - mNavigationViewHeader = mNavigationView.getHeaderView(0); + mNavigationViewHeader = drawerNavigationView.getHeaderView(0); updateHeader(); - setupDrawerMenu(mNavigationView); + setupDrawerMenu(drawerNavigationView); getAndDisplayUserQuota(); setupQuotaElement(); } @@ -232,6 +237,86 @@ public abstract class DrawerActivity extends ToolbarActivity if (getSupportActionBar() != null) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); } + + bottomNavigationView = findViewById(R.id.bottom_navigation); + if (bottomNavigationView != null) { + themeBottomNavigationMenu(); + checkAssistantBottomNavigationMenu(); + handleBottomNavigationViewClicks(); + } + + setNavigationViewItemChecked(); + } + + private void themeBottomNavigationMenu() { + viewThemeUtils.platform.colorBottomNavigationView(bottomNavigationView); + } + + @SuppressFBWarnings("RV") + private void checkAssistantBottomNavigationMenu() { + boolean isAssistantAvailable = getCapabilities().getAssistant().isTrue(); + + bottomNavigationView + .getMenu() + .findItem(R.id.nav_assistant) + .setVisible(isAssistantAvailable); + } + + @SuppressFBWarnings("RV") + private void handleBottomNavigationViewClicks() { + bottomNavigationView.setOnItemSelectedListener(menuItem -> { + menuItemId = menuItem.getItemId(); + + exitSelectionMode(); + resetOnlyPersonalAndOnDevice(); + + if (menuItemId == R.id.nav_all_files) { + showFiles(false,false); + if (this instanceof FileDisplayActivity fda) { + fda.browseToRoot(); + } + EventBus.getDefault().post(new ChangeMenuEvent()); + } else if (menuItemId == R.id.nav_favorites) { + setupToolbar(); + handleSearchEvents(new SearchEvent("", SearchRemoteOperation.SearchType.FAVORITE_SEARCH), menuItemId); + } else if (menuItemId == R.id.nav_assistant && !(this instanceof ComposeActivity)) { + startComposeActivity(ComposeDestination.AssistantScreen, R.string.assistant_screen_top_bar_title); + } else if (menuItemId == R.id.nav_gallery) { + setupToolbar(); + startPhotoSearch(menuItem.getItemId()); + } + + // Remove extra icon from the action bar + if (getSupportActionBar() != null) { + getSupportActionBar().setIcon(null); + } + + setNavigationViewItemChecked(); + + return false; + }); + } + + @Nullable + public OCFileListFragment getOCFileListFragment() { + Fragment fragment = ActivityExtensionsKt.lastFragment(this); + if (fragment instanceof OCFileListFragment fileListFragment) { + return fileListFragment; + } + + fragment = getSupportFragmentManager().findFragmentByTag(FileDisplayActivity.TAG_LIST_OF_FILES); + if (fragment instanceof OCFileListFragment fileListFragment) { + return fileListFragment; + } + + return null; + } + + private void exitSelectionMode() { + Fragment fragment = getOCFileListFragment(); + if (fragment instanceof OCFileListFragment fileListFragment) { + fileListFragment.exitSelectionMode(); + } } /** @@ -239,7 +324,6 @@ public abstract class DrawerActivity extends ToolbarActivity */ private void setupDrawerToggle() { mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, R.string.drawer_open, R.string.drawer_close) { - /** Called when a drawer has settled in a completely closed state. */ public void onDrawerClosed(View view) { super.onDrawerClosed(view); @@ -270,7 +354,9 @@ public abstract class DrawerActivity extends ToolbarActivity R.drawable.ic_arrow_back, null); - viewThemeUtils.platform.tintToolbarArrowDrawable(this, mDrawerToggle, backArrow); + if (backArrow != null) { + viewThemeUtils.platform.tintToolbarArrowDrawable(this, mDrawerToggle, backArrow); + } } /** @@ -285,84 +371,105 @@ public abstract class DrawerActivity extends ToolbarActivity } public void updateHeader() { - int primaryColor = themeColorUtils.unchangedPrimaryColor(getAccount(), this); + final var account = getAccount(); boolean isClientBranded = getResources().getBoolean(R.bool.is_branded_client); + final OCCapability capability = getCapabilities(); - if (getAccount() != null && - getCapabilities().getServerBackground() != null && !isClientBranded) { - - OCCapability capability = getCapabilities(); - String logo = capability.getServerLogo(); + if (capability != null && account != null && capability.getServerBackground() != null && !isClientBranded) { + int primaryColor = themeColorUtils.unchangedPrimaryColor(account, this); + String serverLogoURL = capability.getServerLogo(); // set background to primary color LinearLayout drawerHeader = mNavigationViewHeader.findViewById(R.id.drawer_header_view); drawerHeader.setBackgroundColor(primaryColor); - if (!TextUtils.isEmpty(logo) && URLUtil.isValidUrl(logo)) { - // background image - GenericRequestBuilder requestBuilder = Glide.with(this) - .using(Glide.buildStreamModelLoader(Uri.class, this), InputStream.class) - .from(Uri.class) - .as(SVGorImage.class) - .transcode(new SvgOrImageBitmapTranscoder(128, 128), Bitmap.class) - .sourceEncoder(new StreamEncoder()) - .cacheDecoder(new FileToStreamDecoder<>(new SvgOrImageDecoder())) - .decoder(new SvgOrImageDecoder()); - - // background image - SimpleTarget target = new SimpleTarget<>() { - @Override - public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) { - - Bitmap logo = resource; - int width = resource.getWidth(); - int height = resource.getHeight(); - int max = Math.max(width, height); - if (max > MAX_LOGO_SIZE_PX) { - logo = BitmapUtils.scaleBitmap(resource, MAX_LOGO_SIZE_PX, width, height, max); - } - - Drawable[] drawables = {new ColorDrawable(primaryColor), - new BitmapDrawable(getResources(), logo)}; - LayerDrawable layerDrawable = new LayerDrawable(drawables); - - String name = capability.getServerName(); - setDrawerHeaderLogo(layerDrawable, name); - } - }; - - requestBuilder - .diskCacheStrategy(DiskCacheStrategy.SOURCE) - .load(Uri.parse(logo)) - .into(target); + if (!TextUtils.isEmpty(serverLogoURL) && URLUtil.isValidUrl(serverLogoURL)) { + Target target = createSVGLogoTarget(primaryColor, capability); + getClientRepository().getNextcloudClient(nextcloudClient -> { + GlideHelper.INSTANCE.loadIntoTarget(DrawerActivity.this, + nextcloudClient, + serverLogoURL, + target, + R.drawable.background); + return Unit.INSTANCE; + }); } } // hide ecosystem apps according to user preference or in branded client - LinearLayout banner = mNavigationViewHeader.findViewById(R.id.drawer_ecosystem_apps); + ConstraintLayout banner = mNavigationViewHeader.findViewById(R.id.drawer_ecosystem_apps); boolean shouldHideTopBanner = isClientBranded || !preferences.isShowEcosystemApps(); if (shouldHideTopBanner) { hideTopBanner(banner); } else { - showTopBanner(banner, primaryColor); + showTopBanner(banner); } } - private void hideTopBanner(LinearLayout banner) { + private Target createSVGLogoTarget(int primaryColor, OCCapability capability) { + return new CustomTarget<>() { + @Override + public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { + Bitmap bitmap; + + if (resource instanceof PictureDrawable pictureDrawable) { + bitmap = Bitmap.createBitmap( + pictureDrawable.getIntrinsicWidth(), + pictureDrawable.getIntrinsicHeight(), + Bitmap.Config.ARGB_8888); + + Canvas canvas = new Canvas(bitmap); + canvas.drawPicture(pictureDrawable.getPicture()); + + } else if (resource instanceof BitmapDrawable bitmapDrawable) { + bitmap = bitmapDrawable.getBitmap(); + } else { + Log_OC.e(TAG, "Unsupported drawable type: " + resource.getClass().getName()); + return; + } + + // Scale down if necessary + Bitmap logo = bitmap; + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + int max = Math.max(width, height); + if (max > MAX_LOGO_SIZE_PX) { + logo = BitmapUtils.scaleBitmap(bitmap, MAX_LOGO_SIZE_PX, width, height, max); + } + + Drawable[] drawables = { + new ColorDrawable(primaryColor), + new BitmapDrawable(getResources(), logo) + }; + LayerDrawable layerDrawable = new LayerDrawable(drawables); + + String name = capability.getServerName(); + setDrawerHeaderLogo(layerDrawable, name); + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) {} + }; + } + + private void hideTopBanner(ConstraintLayout banner) { banner.setVisibility(View.GONE); } - private void showTopBanner(LinearLayout banner, int primaryColor) { + private void showTopBanner(ConstraintLayout banner) { LinearLayout notesView = banner.findViewById(R.id.drawer_ecosystem_notes); LinearLayout talkView = banner.findViewById(R.id.drawer_ecosystem_talk); LinearLayout moreView = banner.findViewById(R.id.drawer_ecosystem_more); LinearLayout assistantView = banner.findViewById(R.id.drawer_ecosystem_assistant); - notesView.setOnClickListener(v -> openAppOrStore("it.niedermann.owncloud.notes")); - talkView.setOnClickListener(v -> openAppOrStore("com.nextcloud.talk2")); - moreView.setOnClickListener(v -> openAppStore("Nextcloud", true)); - assistantView.setOnClickListener(v -> startComposeActivity(ComposeDestination.AssistantScreen, R.string.assistant_screen_top_bar_title, -1)); + notesView.setOnClickListener(v -> LinkHelper.INSTANCE.openAppOrStore(LinkHelper.APP_NEXTCLOUD_NOTES, getUser(), this)); + talkView.setOnClickListener(v -> LinkHelper.INSTANCE.openAppOrStore(LinkHelper.APP_NEXTCLOUD_TALK, getUser(), this)); + moreView.setOnClickListener(v -> LinkHelper.INSTANCE.openAppStore("Nextcloud", true, this)); + assistantView.setOnClickListener(v -> { + DrawerActivity.menuItemId = Menu.NONE; + startComposeActivity(ComposeDestination.AssistantScreen, R.string.assistant_screen_top_bar_title); + }); if (getCapabilities() != null && getCapabilities().getAssistant().isTrue()) { assistantView.setVisibility(View.VISIBLE); } else { @@ -372,8 +479,14 @@ public abstract class DrawerActivity extends ToolbarActivity List views = Arrays.asList(notesView, talkView, moreView, assistantView); int iconColor; - if (Hct.fromInt(primaryColor).getTone() < 80.0) { - iconColor = Color.WHITE; + final var account = getAccount(); + if (account != null) { + int primaryColor = themeColorUtils.unchangedPrimaryColor(account, this); + if (Hct.fromInt(primaryColor).getTone() < 80.0) { + iconColor = Color.WHITE; + } else { + iconColor = getColor(R.color.grey_800_transparent); + } } else { iconColor = getColor(R.color.grey_800_transparent); } @@ -390,45 +503,6 @@ public abstract class DrawerActivity extends ToolbarActivity banner.setVisibility(View.VISIBLE); } - /** - * Open specified app and, if not installed redirect to corresponding download. - * - * @param packageName of app to be opened - */ - private void openAppOrStore(String packageName) { - Intent intent = getPackageManager().getLaunchIntentForPackage(packageName); - if (intent != null) { - // app installed - open directly - intent.putExtra(FileDisplayActivity.KEY_ACCOUNT, getUser().get().hashCode()); - startActivity(intent); - } else { - // app not found - open market (Google Play Store, F-Droid, etc.) - openAppStore(packageName, false); - } - } - - /** - * Open app store page of specified app or search for specified string. Will attempt to open browser when no app - * store is available. - * - * @param string packageName or url-encoded search string - * @param search false -> show app corresponding to packageName; true -> open search for string - */ - private void openAppStore(String string, boolean search) { - String suffix = (search ? "search?q=" : "details?id=") + string; - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://" + suffix)); - try { - startActivity(intent); - } catch (android.content.ActivityNotFoundException activityNotFoundException1) { - // all is lost: open google play store web page for app - if (!search) { - suffix = "apps/" + suffix; - } - intent.setData(Uri.parse("https://play.google.com/store/" + suffix)); - startActivity(intent); - } - } - private void setDrawerHeaderLogo(Drawable drawable, String serverName) { ImageView imageHeader = mNavigationViewHeader.findViewById(R.id.drawer_header_logo); imageHeader.setImageDrawable(drawable); @@ -449,7 +523,6 @@ public abstract class DrawerActivity extends ToolbarActivity * @param navigationView the drawers navigation view */ private void setupDrawerMenu(NavigationView navigationView) { - navigationView.setItemIconTintList(null); // setup actions for drawer menu items navigationView.setNavigationItemSelectedListener( @@ -460,7 +533,6 @@ public abstract class DrawerActivity extends ToolbarActivity return true; }); - User account = accountManager.getUser(); filterDrawerMenu(navigationView.getMenu(), account); } @@ -485,57 +557,65 @@ public abstract class DrawerActivity extends ToolbarActivity } private void onNavigationItemClicked(final MenuItem menuItem) { - setDrawerMenuItemChecked(menuItem.getItemId()); - int itemId = menuItem.getItemId(); + menuItemId = itemId; + setNavigationViewItemChecked(); if (itemId == R.id.nav_all_files || itemId == R.id.nav_personal_files) { - if (this instanceof FileDisplayActivity && - !(((FileDisplayActivity) this).getLeftFragment() instanceof GalleryFragment) && - !(((FileDisplayActivity) this).getLeftFragment() instanceof SharedListFragment) && - !(((FileDisplayActivity) this).getLeftFragment() instanceof GroupfolderListFragment) && - !(((FileDisplayActivity) this).getLeftFragment() instanceof PreviewTextStringFragment)) { + if (this instanceof FileDisplayActivity fda && + !(fda.getLeftFragment() instanceof GalleryFragment) && + !(fda.getLeftFragment() instanceof SharedListFragment) && + !(fda.getLeftFragment() instanceof GroupfolderListFragment) && + !(fda.getLeftFragment() instanceof PreviewTextStringFragment)) { showFiles(false, itemId == R.id.nav_personal_files); - ((FileDisplayActivity) this).browseToRoot(); + fda.browseToRoot(); EventBus.getDefault().post(new ChangeMenuEvent()); } else { MainApp.showOnlyFilesOnDevice(false); MainApp.showOnlyPersonalFiles(itemId == R.id.nav_personal_files); Intent intent = new Intent(getApplicationContext(), FileDisplayActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - - if (this instanceof ComposeActivity) { - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - } - intent.setAction(FileDisplayActivity.ALL_FILES); - intent.putExtra(FileDisplayActivity.DRAWER_MENU_ID, menuItem.getItemId()); startActivity(intent); } closeDrawer(); } else if (itemId == R.id.nav_favorites) { - handleSearchEvents(new SearchEvent("", SearchRemoteOperation.SearchType.FAVORITE_SEARCH), - menuItem.getItemId()); + resetOnlyPersonalAndOnDevice(); + setupToolbar(); + handleSearchEvents(new SearchEvent("", SearchRemoteOperation.SearchType.FAVORITE_SEARCH), menuItem.getItemId()); } else if (itemId == R.id.nav_gallery) { + resetOnlyPersonalAndOnDevice(); + setupToolbar(); startPhotoSearch(menuItem.getItemId()); } else if (itemId == R.id.nav_on_device) { EventBus.getDefault().post(new ChangeMenuEvent()); showFiles(true, false); } else if (itemId == R.id.nav_uploads) { + resetOnlyPersonalAndOnDevice(); startActivity(UploadListActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP); } else if (itemId == R.id.nav_trashbin) { + resetOnlyPersonalAndOnDevice(); startActivity(TrashbinActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP); } else if (itemId == R.id.nav_activity) { + resetOnlyPersonalAndOnDevice(); startActivity(ActivitiesActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP); - } else if (itemId == R.id.nav_notifications) { - startActivity(NotificationsActivity.class); } else if (itemId == R.id.nav_settings) { - startActivity(SettingsActivity.class); + resetOnlyPersonalAndOnDevice(); + + /** + * Since pressing the back button in SettingsActivity always returns to the all file list, we can clear the stack. + * {@link SettingsActivity#onBackPressed() + */ + final Intent intent = new Intent(this, SettingsActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); } else if (itemId == R.id.nav_community) { + resetOnlyPersonalAndOnDevice(); startActivity(CommunityActivity.class); } else if (itemId == R.id.nav_logout) { - mCheckedMenuItem = -1; + resetOnlyPersonalAndOnDevice(); + menuItemId = Menu.NONE; MenuItem isNewMenuItemChecked = menuItem.setChecked(false); Log_OC.d(TAG,"onNavigationItemClicked nav_logout setChecked " + isNewMenuItemChecked); final Optional optionalUser = getUser(); @@ -543,17 +623,19 @@ public abstract class DrawerActivity extends ToolbarActivity UserInfoActivity.openAccountRemovalDialog(optionalUser.get(), getSupportFragmentManager()); } } else if (itemId == R.id.nav_shared) { + resetOnlyPersonalAndOnDevice(); startSharedSearch(menuItem); } else if (itemId == R.id.nav_recently_modified) { + resetOnlyPersonalAndOnDevice(); startRecentlyModifiedSearch(menuItem); } else if (itemId == R.id.nav_assistant) { - startComposeActivity(ComposeDestination.AssistantScreen, R.string.assistant_screen_top_bar_title, itemId); + resetOnlyPersonalAndOnDevice(); + startComposeActivity(ComposeDestination.AssistantScreen, R.string.assistant_screen_top_bar_title); } else if (itemId == R.id.nav_groupfolders) { - MainApp.showOnlyFilesOnDevice(false); + resetOnlyPersonalAndOnDevice(); Intent intent = new Intent(getApplicationContext(), FileDisplayActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); intent.setAction(FileDisplayActivity.LIST_GROUPFOLDERS); - intent.putExtra(FileDisplayActivity.DRAWER_MENU_ID, menuItem.getItemId()); startActivity(intent); } else { if (menuItem.getItemId() >= MENU_ITEM_EXTERNAL_LINK && @@ -566,15 +648,14 @@ public abstract class DrawerActivity extends ToolbarActivity } } - private void startComposeActivity(ComposeDestination destination, int titleId, int menuItemId) { + private void startComposeActivity(ComposeDestination destination, int titleId) { Intent composeActivity = new Intent(getApplicationContext(), ComposeActivity.class); composeActivity.putExtra(ComposeActivity.DESTINATION, destination); composeActivity.putExtra(ComposeActivity.TITLE, titleId); - composeActivity.putExtra(ComposeActivity.MENU_ITEM, menuItemId); startActivity(composeActivity); } - private void startActivity(Class activity) { + void startActivity(Class activity) { startActivity(new Intent(getApplicationContext(), activity)); } @@ -595,10 +676,7 @@ public abstract class DrawerActivity extends ToolbarActivity } public void openAddAccount() { - boolean isProviderOrOwnInstallationVisible = getResources() - .getBoolean(R.bool.show_provider_or_own_installation); - - if (isProviderOrOwnInstallationVisible) { + if (MDMConfig.INSTANCE.showIntro(this)) { Intent firstRunIntent = new Intent(getApplicationContext(), FirstRunActivity.class); firstRunIntent.putExtra(FirstRunActivity.EXTRA_ALLOW_CLOSE, true); startActivity(firstRunIntent); @@ -642,16 +720,11 @@ public abstract class DrawerActivity extends ToolbarActivity } private void launchActivityForSearch(SearchEvent searchEvent, int menuItemId) { + DrawerActivity.menuItemId = menuItemId; Intent intent = new Intent(getApplicationContext(), FileDisplayActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - - if (this instanceof ComposeActivity) { - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - } - intent.setAction(Intent.ACTION_SEARCH); intent.putExtra(OCFileListFragment.SEARCH_EVENT, searchEvent); - intent.putExtra(FileDisplayActivity.DRAWER_MENU_ID, menuItemId); startActivity(intent); } @@ -679,7 +752,6 @@ public abstract class DrawerActivity extends ToolbarActivity externalWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_TITLE, link.getName()); externalWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_URL, link.getUrl()); externalWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, true); - externalWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_MENU_ITEM_ID, menuItem.getItemId()); startActivity(externalWebViewIntent); } } @@ -805,26 +877,26 @@ public abstract class DrawerActivity extends ToolbarActivity } private void unsetAllDrawerMenuItems() { - if (mNavigationView != null) { - mNavigationView.getMenu(); - Menu menu = mNavigationView.getMenu(); + if (drawerNavigationView != null) { + drawerNavigationView.getMenu(); + Menu menu = drawerNavigationView.getMenu(); for (int i = 0; i < menu.size(); i++) { menu.getItem(i).setChecked(false); } } - mCheckedMenuItem = Menu.NONE; + menuItemId = Menu.NONE; } private void updateQuotaLink() { if (mQuotaTextLink != null) { - if (getBaseContext().getResources().getBoolean(R.bool.show_external_links)) { + if (MDMConfig.INSTANCE.externalSiteSupport(this)) { List quotas = externalLinksProvider.getExternalLink(ExternalLinkType.QUOTA); float density = getResources().getDisplayMetrics().density; final int size = Math.round(24 * density); - if (quotas.size() > 0) { + if (!quotas.isEmpty()) { final ExternalLink firstQuota = quotas.get(0); mQuotaTextLink.setText(firstQuota.getName()); mQuotaTextLink.setClickable(true); @@ -834,37 +906,19 @@ public abstract class DrawerActivity extends ToolbarActivity externalWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_TITLE, firstQuota.getName()); externalWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_URL, firstQuota.getUrl()); externalWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, true); - externalWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_MENU_ITEM_ID, -1); + menuItemId = Menu.NONE; startActivity(externalWebViewIntent); }); - - SimpleTarget target = new SimpleTarget() { - @Override - public void onResourceReady(Drawable resource, GlideAnimation glideAnimation) { - Drawable test = resource.getCurrent(); - test.setBounds(0, 0, size, size); - mQuotaTextLink.setCompoundDrawablesWithIntrinsicBounds(test, null, null, null); - } - - @Override - public void onLoadFailed(Exception e, Drawable errorDrawable) { - super.onLoadFailed(e, errorDrawable); - - Drawable test = errorDrawable.getCurrent(); - test.setBounds(0, 0, size, size); - - mQuotaTextLink.setCompoundDrawablesWithIntrinsicBounds(test, null, null, null); - } - }; - - DisplayUtils.downloadIcon(getUserAccountManager(), - clientFactory, - this, - firstQuota.getIconUrl(), - target, - R.drawable.ic_link); - + Target quotaTarget = createQuotaDrawableTarget(size, mQuotaTextLink); + getClientRepository().getNextcloudClient(nextcloudClient -> { + GlideHelper.INSTANCE.loadIntoTarget(this, + nextcloudClient, + firstQuota.getIconUrl(), + quotaTarget, + R.drawable.ic_link); + return Unit.INSTANCE; + }); } else { mQuotaTextLink.setVisibility(View.GONE); } @@ -874,19 +928,58 @@ public abstract class DrawerActivity extends ToolbarActivity } } + private Target createQuotaDrawableTarget(int size, TextView quotaTextLink) { + return new CustomTarget<>() { + @Override + public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { + Drawable drawable = resource.getCurrent(); + drawable.setBounds(0, 0, size, size); + quotaTextLink.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + + } + + @Override + public void onLoadFailed(@Nullable Drawable errorDrawable) { + super.onLoadFailed(errorDrawable); + + Drawable drawable = errorDrawable != null ? errorDrawable.getCurrent() : null; + if (drawable != null) { + drawable.setBounds(0, 0, size, size); + quotaTextLink.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null); + } + } + }; + } + + /** - * checks/highlights the provided menu item if the drawer has been initialized and the menu item exists. - * - * @param menuItemId the menu item to be highlighted + * Sets the menu item as checked in both the drawer and bottom navigation views, if applicable. */ - protected void setDrawerMenuItemChecked(int menuItemId) { - if (mNavigationView != null && mNavigationView.getMenu().findItem(menuItemId) != null) { - viewThemeUtils.platform.colorNavigationView(mNavigationView); - mCheckedMenuItem = menuItemId; - mNavigationView.getMenu().findItem(menuItemId).setChecked(true); - } else { - Log_OC.w(TAG, "setDrawerMenuItemChecked has been called with invalid menu-item-ID"); + @SuppressFBWarnings("RV") + public void setNavigationViewItemChecked() { + if (drawerNavigationView != null) { + MenuItem menuItem = drawerNavigationView.getMenu().findItem(menuItemId); + + if (menuItem != null && !menuItem.isChecked()) { + viewThemeUtils.platform.colorNavigationView(drawerNavigationView); + menuItem.setChecked(true); + } } + + if (bottomNavigationView != null) { + MenuItem menuItem = bottomNavigationView.getMenu().findItem(menuItemId); + + // Don't highlight assistant bottom navigation item because Assistant screen doesn't have same bottom navigation bar + if (menuItem != null && !menuItem.isChecked() && menuItem.getItemId() != R.id.nav_assistant) { + menuItem.setChecked(true); + } + } + + Log_OC.d(TAG, "New menu item is: " + menuItemId); } /** @@ -956,60 +1049,77 @@ public abstract class DrawerActivity extends ToolbarActivity } private void updateExternalLinksInDrawer() { - if (mNavigationView != null && getBaseContext().getResources().getBoolean(R.bool.show_external_links)) { - mNavigationView.getMenu().removeGroup(R.id.drawer_menu_external_links); + if (drawerNavigationView == null || !MDMConfig.INSTANCE.externalSiteSupport(this)) { + return; + } - int greyColor = ContextCompat.getColor(this, R.color.drawer_menu_icon); + drawerNavigationView.getMenu().removeGroup(R.id.drawer_menu_external_links); - for (final ExternalLink link : externalLinksProvider.getExternalLink(ExternalLinkType.LINK)) { - int id = mNavigationView.getMenu().add(R.id.drawer_menu_external_links, - MENU_ITEM_EXTERNAL_LINK + link.getId(), MENU_ORDER_EXTERNAL_LINKS, link.getName()) - .setCheckable(true).getItemId(); + int greyColor = ContextCompat.getColor(this, R.color.drawer_menu_icon); - MenuSimpleTarget target = new MenuSimpleTarget(id) { - @Override - public void onResourceReady(Drawable resource, GlideAnimation glideAnimation) { - setExternalLinkIcon(getIdMenuItem(), resource, greyColor); - } + for (final ExternalLink link : externalLinksProvider.getExternalLink(ExternalLinkType.LINK)) { + int id = drawerNavigationView + .getMenu() + .add(R.id.drawer_menu_external_links, + MENU_ITEM_EXTERNAL_LINK + + link.getId(), MENU_ORDER_EXTERNAL_LINKS, + link.getName() + ) + .setCheckable(true) + .getItemId(); - @Override - public void onLoadFailed(Exception e, Drawable errorDrawable) { - super.onLoadFailed(e, errorDrawable); - setExternalLinkIcon(getIdMenuItem(), errorDrawable, greyColor); - } - }; - - DisplayUtils.downloadIcon(getUserAccountManager(), - clientFactory, - this, - link.getIconUrl(), - target, - R.drawable.ic_link); - } - - setDrawerMenuItemChecked(mCheckedMenuItem); + Target iconTarget = createMenuItemTarget(id, greyColor); + getClientRepository().getNextcloudClient(nextcloudClient -> { + GlideHelper.INSTANCE.loadIntoTarget( + this, + nextcloudClient, + link.getIconUrl(), + iconTarget, + R.drawable.ic_link); + return Unit.INSTANCE; + }); } } - private void setExternalLinkIcon(int id, Drawable drawable, int greyColor) { - MenuItem menuItem = mNavigationView.getMenu().findItem(id); - - if (menuItem != null) { - if (drawable != null) { - menuItem.setIcon(viewThemeUtils.platform.colorDrawable(drawable, greyColor)); - } else { - menuItem.setIcon(R.drawable.ic_link); + private Target createMenuItemTarget(int menuItemId, int tintColor) { + return new CustomTarget<>() { + @Override + public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { + setExternalLinkIcon(menuItemId, resource, tintColor); } + + @Override + public void onLoadFailed(@Nullable Drawable errorDrawable) { + setExternalLinkIcon(menuItemId, errorDrawable, tintColor); + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + + } + }; + } + + private void setExternalLinkIcon(int id, Drawable drawable, int greyColor) { + MenuItem menuItem = drawerNavigationView.getMenu().findItem(id); + if (menuItem == null) { + return; } + + if (drawable == null) { + menuItem.setIcon(R.drawable.ic_link); + return; + } + + final var resizedDrawable = DrawableUtil.INSTANCE.getResizedDrawable(this, drawable,32); + menuItem.setIcon(viewThemeUtils.platform.colorDrawable(resizedDrawable, greyColor)); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (savedInstanceState != null) { mIsAccountChooserActive = savedInstanceState.getBoolean(KEY_IS_ACCOUNT_CHOOSER_ACTIVE, false); - mCheckedMenuItem = savedInstanceState.getInt(KEY_CHECKED_MENU_ITEM, Menu.NONE); } externalLinksProvider = new ExternalLinksProvider(getContentResolver()); @@ -1019,22 +1129,14 @@ public abstract class DrawerActivity extends ToolbarActivity @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); - outState.putBoolean(KEY_IS_ACCOUNT_CHOOSER_ACTIVE, mIsAccountChooserActive); - outState.putInt(KEY_CHECKED_MENU_ITEM, mCheckedMenuItem); } @Override public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); - mIsAccountChooserActive = savedInstanceState.getBoolean(KEY_IS_ACCOUNT_CHOOSER_ACTIVE, false); - mCheckedMenuItem = savedInstanceState.getInt(KEY_CHECKED_MENU_ITEM, Menu.NONE); - - // check/highlight the menu item if present - if (mCheckedMenuItem > Menu.NONE || mCheckedMenuItem < Menu.NONE) { - setDrawerMenuItemChecked(mCheckedMenuItem); - } + setNavigationViewItemChecked(); } @Override @@ -1051,10 +1153,6 @@ public abstract class DrawerActivity extends ToolbarActivity updateQuotaLink(); } - public int getCheckedMenuItem() { - return mCheckedMenuItem; - } - @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); @@ -1063,25 +1161,24 @@ public abstract class DrawerActivity extends ToolbarActivity } } - @Override - public void onBackPressed() { - if (isDrawerOpen()) { - closeDrawer(); - return; - } - Fragment fileDetailsSharingProcessFragment = - getSupportFragmentManager().findFragmentByTag(FileDetailsSharingProcessFragment.TAG); - if (fileDetailsSharingProcessFragment != null) { - ((FileDetailsSharingProcessFragment) fileDetailsSharingProcessFragment).onBackPressed(); - } else { - super.onBackPressed(); - } - } + public void addOnBackPressedCallback() { + getOnBackPressedDispatcher().addCallback(new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (isDrawerOpen()) { + closeDrawer(); + return; + } - @Override - protected void onResume() { - super.onResume(); - setDrawerMenuItemChecked(mCheckedMenuItem); + final var fragment = getSupportFragmentManager().findFragmentByTag(FileDetailsSharingProcessFragment.TAG); + if (fragment instanceof FileDetailsSharingProcessFragment fileDetailsSharingProcessFragment) { + fileDetailsSharingProcessFragment.onBackPressed(); + } else { + setEnabled(false); + getOnBackPressedDispatcher().onBackPressed(); + } + } + }); } @Override @@ -1139,6 +1236,11 @@ public abstract class DrawerActivity extends ToolbarActivity fetchExternalLinks(false); } + private void resetOnlyPersonalAndOnDevice() { + MainApp.showOnlyFilesOnDevice(false); + MainApp.showOnlyPersonalFiles(false); + } + /** * show the file list to the user. * @@ -1147,15 +1249,11 @@ public abstract class DrawerActivity extends ToolbarActivity public void showFiles(boolean onDeviceOnly, boolean onlyPersonalFiles) { MainApp.showOnlyFilesOnDevice(onDeviceOnly); MainApp.showOnlyPersonalFiles(onlyPersonalFiles); - Intent fileDisplayActivity = new Intent(getApplicationContext(), FileDisplayActivity.class); - fileDisplayActivity.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - if (this instanceof ComposeActivity) { - fileDisplayActivity.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - } - - fileDisplayActivity.setAction(FileDisplayActivity.ALL_FILES); - startActivity(fileDisplayActivity); + Intent intent = new Intent(getApplicationContext(), FileDisplayActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.setAction(FileDisplayActivity.ALL_FILES); + startActivity(intent); } @Override @@ -1227,7 +1325,7 @@ public abstract class DrawerActivity extends ToolbarActivity * Retrieves external links via api from 'external' app */ public void fetchExternalLinks(final boolean force) { - if (!getBaseContext().getResources().getBoolean(R.bool.show_external_links)) { + if (!MDMConfig.INSTANCE.externalSiteSupport(this)) { return; } @@ -1239,7 +1337,8 @@ public abstract class DrawerActivity extends ToolbarActivity Thread t = new Thread(() -> { // fetch capabilities as early as possible - if ((getCapabilities() == null || getCapabilities().getAccountName() != null && getCapabilities().getAccountName().isEmpty()) + final OCCapability capability = getCapabilities(); + if ((capability == null || capability.getAccountName() == null || !capability.getAccountName().isEmpty()) && getStorageManager() != null) { GetCapabilitiesOperation getCapabilities = new GetCapabilitiesOperation(getStorageManager()); getCapabilities.execute(getBaseContext()); @@ -1284,4 +1383,53 @@ public abstract class DrawerActivity extends ToolbarActivity }); t.start(); } + + protected void handleDeepLink(@NonNull Uri uri) { + String path = uri.getLastPathSegment(); + if (path == null) return; + + DeepLinkConstants deepLinkType = DeepLinkConstants.Companion.fromPath(path); + if (deepLinkType == null) { + DisplayUtils.showSnackMessage(this, getString(R.string.invalid_url)); + return; + } + + switch (deepLinkType) { + case OPEN_AUTO_UPLOAD: + startActivity(new Intent(this, SyncedFoldersActivity.class)); + break; + case OPEN_EXTERNAL_URL: + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri.getQueryParameter("url"))); + startActivity(intent); + break; + case ACTION_CREATE_NEW: + findViewById(R.id.fab_main).callOnClick(); + break; + case ACTION_APP_UPDATE: + LinkHelper.INSTANCE.openAppStore(getPackageName(), false, this); + break; + case OPEN_NOTIFICATIONS: + startActivity(NotificationsActivity.class); + break; + default: + handleNavItemClickEvent(deepLinkType.getNavId()); + break; + } + } + + private void handleNavItemClickEvent(@IdRes int menuItemId) { + if (drawerNavigationView == null) { + drawerNavigationView = findViewById(R.id.nav_view); + } + Menu navMenu = drawerNavigationView.getMenu(); + onNavigationItemClicked(navMenu.findItem(menuItemId)); + } + + public void showBottomNavigationBar(boolean show) { + ViewExtensionsKt.setVisibleIf(bottomNavigationView, show); + } + + public BottomNavigationView getBottomNavigationView() { + return bottomNavigationView; + } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.java b/app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.java index de413a8..132957c 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/EditorWebView.java @@ -3,18 +3,20 @@ * * SPDX-FileCopyrightText: 2019 Tobias Kaminsky * SPDX-FileCopyrightText: 2019 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activity; import android.app.DownloadManager; import android.content.ActivityNotFoundException; +import android.content.ClipData; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.net.Uri; +import android.os.Environment; import android.os.Handler; import android.view.View; import android.webkit.JavascriptInterface; @@ -31,10 +33,12 @@ import com.owncloud.android.databinding.RichdocumentsWebviewBinding; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.datamodel.SyncedFolderProvider; import com.owncloud.android.datamodel.ThumbnailsCacheManager; +import com.owncloud.android.ui.asynctasks.TextEditorLoadUrlTask; import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.MimeTypeUtil; import com.owncloud.android.utils.WebViewUtil; +import java.util.ArrayList; import java.util.Optional; import javax.inject.Inject; @@ -69,12 +73,15 @@ public abstract class EditorWebView extends ExternalSiteWebView { this.url = loadedUrl; if (!url.isEmpty()) { - new WebViewUtil(getApplicationContext()).setProxyKKPlus(this.getWebView()); + new WebViewUtil().setProxyKKPlus(this.getWebView()); try { Thread.sleep(1000); - } catch (InterruptedException e) { + } catch (InterruptedException ignored) { + } + + if (!url.equals(this.getWebView().getUrl())) { + this.getWebView().loadUrl(url); } - this.getWebView().loadUrl(url); new Handler().postDelayed(() -> { if (this.getWebView().getVisibility() != View.VISIBLE) { @@ -99,6 +106,23 @@ public abstract class EditorWebView extends ExternalSiteWebView { finish(); } + public void reload() { + if (getWebView().getVisibility() != View.VISIBLE) { + return; + } + + Optional user = getUser(); + if (!user.isPresent()) { + return; + } + + OCFile file = getFile(); + if (file != null) { + TextEditorLoadUrlTask task = new TextEditorLoadUrlTask(this, user.get(), file, editorUtils); + task.execute(); + } + } + @Override protected void bindView() { binding = RichdocumentsWebviewBinding.inflate(getLayoutInflater()); @@ -182,7 +206,20 @@ public abstract class EditorWebView extends ExternalSiteWebView { return; } - uploadMessage.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data)); + if (data.getClipData() == null) { + // one file + uploadMessage.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data)); + } else { + ArrayList uris = new ArrayList<>(); + // multiple files + for (int i = 0; i < data.getClipData().getItemCount(); i++) { + ClipData.Item item = data.getClipData().getItemAt(i); + uris.add(item.getUri()); + } + + uploadMessage.onReceiveValue(uris.toArray(new Uri[0])); + } + uploadMessage = null; } @@ -217,7 +254,7 @@ public abstract class EditorWebView extends ExternalSiteWebView { boolean isAutoUploadFolder = SyncedFolderProvider.isAutoUploadFolder(syncedFolderProvider, file, user); Integer overlayIconId = file.getFileOverlayIconId(isAutoUploadFolder); - LayerDrawable drawable = MimeTypeUtil.getFileIcon(preferences.isDarkModeEnabled(), overlayIconId, this, viewThemeUtils); + LayerDrawable drawable = MimeTypeUtil.getFolderIcon(preferences.isDarkModeEnabled(), overlayIconId, this, viewThemeUtils); binding.thumbnail.setImageDrawable(drawable); } else { if ((MimeTypeUtil.isImage(file) || MimeTypeUtil.isVideo(file)) && file.getRemoteId() != null) { @@ -247,7 +284,7 @@ public abstract class EditorWebView extends ExternalSiteWebView { } } - protected void downloadFile(Uri url) { + protected void downloadFile(Uri url, String fileName) { DownloadManager downloadmanager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); if (downloadmanager == null) { @@ -259,6 +296,10 @@ public abstract class EditorWebView extends ExternalSiteWebView { request.allowScanningByMediaScanner(); request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + // change the name file and your current activity. + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName); + + downloadmanager.enqueue(request); } @@ -281,6 +322,11 @@ public abstract class EditorWebView extends ExternalSiteWebView { public void loaded() { runOnUiThread(EditorWebView.this::hideLoading); } + + @JavascriptInterface + public void reload() { + EditorWebView.this.reload(); + } } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ErrorsWhileCopyingHandlerActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ErrorsWhileCopyingHandlerActivity.java index c75c75e..ac63db3 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ErrorsWhileCopyingHandlerActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ErrorsWhileCopyingHandlerActivity.java @@ -7,10 +7,11 @@ * SPDX-FileCopyrightText: 2015 ownCloud Inc. * SPDX-FileCopyrightText: 2013 María Asensio Valverde * SPDX-FileCopyrightText: 2012-2013 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.ui.activity; +import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.os.AsyncTask; @@ -194,6 +195,7 @@ public class ErrorsWhileCopyingHandlerActivity extends AppCompatActivity implem /** * Asynchronous task performing the move of all the local files to the ownCloud folder. */ + @SuppressLint("StaticFieldLeak") private class MoveFilesTask extends AsyncTask { /** diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java b/app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java index 1bb63ce..dad57ad 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ExternalSiteWebView.java @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2017 Tobias Kaminsky * SPDX-FileCopyrightText: 2017 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activity; @@ -44,14 +44,12 @@ public class ExternalSiteWebView extends FileActivity { public static final String EXTRA_URL = "URL"; public static final String EXTRA_SHOW_SIDEBAR = "SHOW_SIDEBAR"; public static final String EXTRA_SHOW_TOOLBAR = "SHOW_TOOLBAR"; - public static final String EXTRA_MENU_ITEM_ID = "MENU_ITEM_ID"; public static final String EXTRA_TEMPLATE = "TEMPLATE"; private static final String TAG = ExternalSiteWebView.class.getSimpleName(); protected boolean showToolbar = true; private ExternalsiteWebviewBinding binding; - private int menuItemId; private boolean showSidebar; String url; @@ -67,7 +65,6 @@ public class ExternalSiteWebView extends FileActivity { showToolbar = extras.getBoolean(EXTRA_SHOW_TOOLBAR); } - menuItemId = extras.getInt(EXTRA_MENU_ITEM_ID); showSidebar = extras.getBoolean(EXTRA_SHOW_SIDEBAR); // show progress @@ -107,8 +104,7 @@ public class ExternalSiteWebView extends FileActivity { } } - // setup drawer - setupDrawer(menuItemId); + setupDrawer(); if (!showSidebar) { setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); @@ -151,7 +147,7 @@ public class ExternalSiteWebView extends FileActivity { } }); - new WebViewUtil(getApplicationContext()).setProxyKKPlus(getWebView()); + new WebViewUtil().setProxyKKPlus(getWebView()); getWebView().loadUrl(url); } @@ -235,12 +231,6 @@ public class ExternalSiteWebView extends FileActivity { } } - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - setDrawerMenuItemChecked(menuItemId); - } - protected WebView getWebView() { return binding.webView; } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java index b6b4c0b..1e17272 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java @@ -10,7 +10,7 @@ * SPDX-FileCopyrightText: 2015 María Asensio Valverde * SPDX-FileCopyrightText: 2013 David A. Velasco * SPDX-FileCopyrightText: 2011 Bartek Przybylski - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) */ package com.owncloud.android.ui.activity; @@ -21,8 +21,10 @@ import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.PackageManager; +import android.net.ConnectivityManager; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -36,10 +38,13 @@ import com.nextcloud.client.jobs.BackgroundJobManager; import com.nextcloud.client.jobs.download.FileDownloadWorker; import com.nextcloud.client.jobs.upload.FileUploadHelper; import com.nextcloud.client.network.ConnectivityService; +import com.nextcloud.receiver.NetworkChangeListener; +import com.nextcloud.receiver.NetworkChangeReceiver; import com.nextcloud.utils.EditorUtils; -import com.nextcloud.utils.extensions.ActivityExtensionsKt; import com.nextcloud.utils.extensions.BundleExtensionsKt; +import com.nextcloud.utils.extensions.FileExtensionsKt; import com.nextcloud.utils.extensions.IntentExtensionsKt; +import com.nextcloud.utils.mdm.MDMConfig; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.authentication.AuthenticatorActivity; @@ -62,6 +67,7 @@ import com.owncloud.android.lib.resources.shares.ShareType; import com.owncloud.android.operations.CreateShareViaLinkOperation; import com.owncloud.android.operations.CreateShareWithShareeOperation; import com.owncloud.android.operations.GetSharesForFileOperation; +import com.owncloud.android.operations.SetFilesDownloadLimitOperation; import com.owncloud.android.operations.SynchronizeFileOperation; import com.owncloud.android.operations.SynchronizeFolderOperation; import com.owncloud.android.operations.UnshareOperation; @@ -79,17 +85,26 @@ import com.owncloud.android.ui.dialog.ConfirmationDialogFragment; import com.owncloud.android.ui.dialog.LoadingDialog; import com.owncloud.android.ui.dialog.ShareLinkToDialog; import com.owncloud.android.ui.dialog.SslUntrustedCertDialog; +import com.owncloud.android.ui.events.DialogEvent; +import com.owncloud.android.ui.events.DialogEventType; +import com.owncloud.android.ui.events.FavoriteEvent; import com.owncloud.android.ui.fragment.FileDetailFragment; import com.owncloud.android.ui.fragment.FileDetailSharingFragment; import com.owncloud.android.ui.fragment.OCFileListFragment; +import com.owncloud.android.ui.fragment.filesRepository.FilesRepository; +import com.owncloud.android.ui.fragment.filesRepository.RemoteFilesRepository; import com.owncloud.android.ui.helpers.FileOperationsHelper; import com.owncloud.android.ui.preview.PreviewImageActivity; +import com.owncloud.android.ui.preview.PreviewMediaActivity; import com.owncloud.android.utils.ClipboardUtil; import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.ErrorMessageAdapter; import com.owncloud.android.utils.FilesSyncHelper; import com.owncloud.android.utils.theme.ViewThemeUtils; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -111,9 +126,10 @@ import static com.owncloud.android.ui.activity.FileDisplayActivity.TAG_PUBLIC_LI */ public abstract class FileActivity extends DrawerActivity implements OnRemoteOperationListener, ComponentsGetter, SslUntrustedCertDialog.OnSslUntrustedCertListener, - LoadingVersionNumberTask.VersionDevInterface, FileDetailSharingFragment.OnEditShareListener { + LoadingVersionNumberTask.VersionDevInterface, FileDetailSharingFragment.OnEditShareListener, NetworkChangeListener { public static final String EXTRA_FILE = "com.owncloud.android.ui.activity.FILE"; + public static final String EXTRA_FILE_REMOTE_PATH = "com.owncloud.android.ui.activity.FILE_REMOTE_PATH"; public static final String EXTRA_LIVE_PHOTO_FILE = "com.owncloud.android.ui.activity.LIVE.PHOTO.FILE"; public static final String EXTRA_USER = "com.owncloud.android.ui.activity.USER"; public static final String EXTRA_FROM_NOTIFICATION = "com.owncloud.android.ui.activity.FROM_NOTIFICATION"; @@ -156,12 +172,12 @@ public abstract class FileActivity extends DrawerActivity protected FileDownloadWorker.FileDownloadProgressListener fileDownloadProgressListener; protected FileUploadHelper fileUploadHelper = FileUploadHelper.Companion.instance(); + protected boolean isFileDisplayActivityResumed = false; @Inject UserAccountManager accountManager; - @Inject - ConnectivityService connectivityService; + @Inject public ConnectivityService connectivityService; @Inject BackgroundJobManager backgroundJobManager; @@ -175,6 +191,15 @@ public abstract class FileActivity extends DrawerActivity @Inject ArbitraryDataProvider arbitraryDataProvider; + private NetworkChangeReceiver networkChangeReceiver; + + private FilesRepository filesRepository; + + private void registerNetworkChangeReceiver() { + IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); + registerReceiver(networkChangeReceiver, filter); + } + @Override public void showFiles(boolean onDeviceOnly, boolean personalFiles) { // must be specialized in subclasses @@ -197,10 +222,11 @@ public abstract class FileActivity extends DrawerActivity @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + networkChangeReceiver = new NetworkChangeReceiver(this, connectivityService); usersAndGroupsSearchConfig.reset(); mHandler = new Handler(); mFileOperationsHelper = new FileOperationsHelper(this, getUserAccountManager(), connectivityService, editorUtils); - User user = null; + User user; if (savedInstanceState != null) { mFile = BundleExtensionsKt.getParcelableArgument(savedInstanceState, FileActivity.EXTRA_FILE, OCFile.class); @@ -226,11 +252,26 @@ public abstract class FileActivity extends DrawerActivity mOperationsServiceConnection = new OperationsServiceConnection(); bindService(new Intent(this, OperationsService.class), mOperationsServiceConnection, Context.BIND_AUTO_CREATE); + registerNetworkChangeReceiver(); + + filesRepository = new RemoteFilesRepository(getClientRepository(), this); } - public void checkInternetConnection() { - if (connectivityService != null && connectivityService.isConnected()) { + @Override + public void networkAndServerConnectionListener(boolean isNetworkAndServerAvailable) { + if (isNetworkAndServerAvailable) { hideInfoBox(); + + // No need to refresh the file list again since file display activity doing it. + if (!isFileDisplayActivityResumed) { + refreshList(); + } + } else { + if (this instanceof PreviewMediaActivity) { + hideInfoBox(); + } else { + showInfoBox(R.string.offline_mode); + } } } @@ -265,19 +306,26 @@ public abstract class FileActivity extends DrawerActivity mOperationsServiceBinder = null; } + unregisterReceiver(networkChangeReceiver); + super.onDestroy(); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); + FileExtensionsKt.logFileSize(mFile, TAG); outState.putParcelable(FileActivity.EXTRA_FILE, mFile); outState.putBoolean(FileActivity.EXTRA_FROM_NOTIFICATION, mFromNotification); outState.putLong(KEY_WAITING_FOR_OP_ID, mFileOperationsHelper.getOpIdWaitingFor()); - if(getSupportActionBar() != null && getSupportActionBar().getTitle() != null) { - // Null check in case the actionbar is used in ActionBar.NAVIGATION_MODE_LIST - // since it doesn't have a title then - outState.putString(KEY_ACTION_BAR_TITLE, getSupportActionBar().getTitle().toString()); + + final var actionBar = getSupportActionBar(); + + if(actionBar != null) { + final var actionBarTitle = actionBar.getTitle(); + if (actionBarTitle != null) { + outState.putString(KEY_ACTION_BAR_TITLE, actionBarTitle.toString()); + } } } @@ -286,6 +334,7 @@ public abstract class FileActivity extends DrawerActivity * * @return Main {@link OCFile} handled by the activity. */ + @Nullable public OCFile getFile() { return mFile; } @@ -346,7 +395,7 @@ public abstract class FileActivity extends DrawerActivity (result.isException() && result.getException() instanceof AuthenticatorException) )) { - requestCredentialsUpdate(this); + requestCredentialsUpdate(); if (result.getCode() == ResultCode.UNAUTHORIZED) { DisplayUtils.showSnackMessage( @@ -395,6 +444,8 @@ public abstract class FileActivity extends DrawerActivity onUpdateShareInformation(result, R.string.sharee_add_failed); } else if (operation instanceof UpdateShareViaLinkOperation || operation instanceof UpdateShareInfoOperation) { onUpdateShareInformation(result, R.string.updating_share_failed); + } else if (operation instanceof SetFilesDownloadLimitOperation) { + onUpdateShareInformation(result, R.string.set_download_limit_failed); } else if (operation instanceof UpdateSharePermissionsOperation) { onUpdateShareInformation(result, R.string.updating_share_failed); } else if (operation instanceof UnshareOperation) { @@ -410,34 +461,24 @@ public abstract class FileActivity extends DrawerActivity * * Equivalent to call requestCredentialsUpdate(context, null); * - * @param context Android Context needed to access the {@link AccountManager}. Received as a parameter - * to make the method accessible to {@link android.content.BroadcastReceiver}s. */ - protected void requestCredentialsUpdate(Context context) { - requestCredentialsUpdate(context, null); + protected void requestCredentialsUpdate() { + requestCredentialsUpdate(null); } /** * Invalidates the credentials stored for the given OC account and requests new credentials to the user, * navigating to {@link AuthenticatorActivity} * - * @param context Android Context needed to access the {@link AccountManager}. Received as a parameter - * to make the method accessible to {@link android.content.BroadcastReceiver}s. * @param account Stored OC account to request credentials update for. If null, current account will * be used. */ - protected void requestCredentialsUpdate(Context context, Account account) { + protected void requestCredentialsUpdate(Account account) { if (account == null) { account = getAccount(); } - boolean remoteWipeSupported = accountManager.getServerVersion(account).isRemoteWipeSupported(); - - if (remoteWipeSupported) { - new CheckRemoteWipeTask(backgroundJobManager, account, new WeakReference<>(this)).execute(); - } else { - performCredentialsUpdate(account, context); - } + new CheckRemoteWipeTask(backgroundJobManager, account, new WeakReference<>(this)).execute(); } public void performCredentialsUpdate(Account account, Context context) { @@ -516,37 +557,48 @@ public abstract class FileActivity extends DrawerActivity } } - /** * Show loading dialog */ public void showLoadingDialog(String message) { - dismissLoadingDialog(); + runOnUiThread(() -> { + FragmentManager fragmentManager = getSupportFragmentManager(); + Fragment existingDialog = fragmentManager.findFragmentByTag(DIALOG_WAIT_TAG); - Fragment frag = getSupportFragmentManager().findFragmentByTag(DIALOG_WAIT_TAG); - if (frag == null) { - Log_OC.d(TAG, "show loading dialog"); - LoadingDialog loading = LoadingDialog.newInstance(message); - FragmentManager fm = getSupportFragmentManager(); - FragmentTransaction ft = fm.beginTransaction(); - ft.add(loading, DIALOG_WAIT_TAG); - ft.commitAllowingStateLoss(); - } + if (existingDialog instanceof LoadingDialog loadingDialog) { + Log_OC.d(TAG, "dismiss previous loading dialog"); + loadingDialog.dismiss(); + } + + // Show new dialog + if (!fragmentManager.isStateSaved()) { + Log_OC.d(TAG, "show loading dialog"); + LoadingDialog loadingDialogFragment = LoadingDialog.newInstance(message); + loadingDialogFragment.show(fragmentManager, DIALOG_WAIT_TAG); + } + }); } /** * Dismiss loading dialog */ public void dismissLoadingDialog() { - Fragment frag = getSupportFragmentManager().findFragmentByTag(DIALOG_WAIT_TAG); - if (frag != null) { - Log_OC.d(TAG, "dismiss loading dialog"); - LoadingDialog loadingDialogFragment = (LoadingDialog) frag; - boolean isDialogFragmentReady = ActivityExtensionsKt.isDialogFragmentReady(this, loadingDialogFragment); - if (isDialogFragmentReady) { - loadingDialogFragment.dismiss(); + runOnUiThread(() -> { + FragmentManager fragmentManager = getSupportFragmentManager(); + Fragment fragment = fragmentManager.findFragmentByTag(DIALOG_WAIT_TAG); + + if (fragment instanceof LoadingDialog loadingDialogFragment) { + Log_OC.d(TAG, "dismiss loading dialog"); + + // Avoid dismissing after state is saved + if (!fragmentManager.isStateSaved()) { + loadingDialogFragment.dismiss(); + } else { + // Dismiss allowing state loss if needed + loadingDialogFragment.dismissAllowingStateLoss(); + } } - } + }); } private void doOnResumeAndBound() { @@ -603,6 +655,7 @@ public abstract class FileActivity extends DrawerActivity return fileUploadHelper; } + @Nullable public OCFile getCurrentDir() { OCFile file = getFile(); if (file != null) { @@ -657,7 +710,7 @@ public abstract class FileActivity extends DrawerActivity Integer latestVersion, boolean openDirectly, boolean inBackground) { - Integer currentVersion = -1; + int currentVersion = -1; try { currentVersion = activity.getPackageManager().getPackageInfo(activity.getPackageName(), 0).versionCode; } catch (PackageManager.NameNotFoundException e) { @@ -674,9 +727,7 @@ public abstract class FileActivity extends DrawerActivity } else { Snackbar.make(activity.findViewById(android.R.id.content), R.string.dev_version_new_version_available, Snackbar.LENGTH_LONG) - .setAction(activity.getString(R.string.version_dev_download), v -> { - DisplayUtils.startLinkIntent(activity, devApkLink); - }).show(); + .setAction(activity.getString(R.string.version_dev_download), v -> DisplayUtils.startLinkIntent(activity, devApkLink)).show(); } } else { if (!inBackground) { @@ -689,12 +740,14 @@ public abstract class FileActivity extends DrawerActivity OCFile file, String link, final ViewThemeUtils viewThemeUtils) { - ClipboardUtil.copyToClipboard(activity, link, false); - Snackbar snackbar = Snackbar.make(activity.findViewById(android.R.id.content), R.string.clipboard_text_copied, - Snackbar.LENGTH_LONG) - .setAction(R.string.share, v -> showShareLinkDialog(activity, file, link)); - viewThemeUtils.material.themeSnackbar(snackbar); - snackbar.show(); + if (MDMConfig.INSTANCE.shareViaLink(activity) && MDMConfig.INSTANCE.clipBoardSupport(activity)) { + ClipboardUtil.copyToClipboard(activity, link, false); + Snackbar snackbar = Snackbar.make(activity.findViewById(android.R.id.content), R.string.clipboard_text_copied, + Snackbar.LENGTH_LONG) + .setAction(R.string.share, v -> showShareLinkDialog(activity, file, link)); + viewThemeUtils.material.themeSnackbar(snackbar); + snackbar.show(); + } } public static void showShareLinkDialog(FileActivity activity, ServerFileInterface file, String link) { @@ -789,11 +842,14 @@ public abstract class FileActivity extends DrawerActivity String link = ""; OCFile file = null; for (Object object : result.getData()) { - OCShare shareLink = (OCShare) object; - if (TAG_PUBLIC_LINK.equalsIgnoreCase(shareLink.getShareType().name())) { - link = shareLink.getShareLink(); - file = getStorageManager().getFileByPath(shareLink.getPath()); - break; + if (object instanceof OCShare shareLink) { + ShareType shareType = shareLink.getShareType(); + + if (shareType != null && TAG_PUBLIC_LINK.equalsIgnoreCase(shareType.name())) { + link = shareLink.getShareLink(); + file = getStorageManager().getFileByEncryptedRemotePath(shareLink.getPath()); + break; + } } } @@ -803,8 +859,12 @@ public abstract class FileActivity extends DrawerActivity sharingFragment.onUpdateShareInformation(result, file); } - if (fileListFragment instanceof OCFileListFragment && file != null) { - ((OCFileListFragment) fileListFragment).updateOCFile(file); + if (fileListFragment instanceof OCFileListFragment ocFileListFragment && file != null) { + if (ocFileListFragment.getAdapterFiles().contains(file)) { + ocFileListFragment.updateOCFile(file); + } else { + DisplayUtils.showSnackMessage(this, R.string.file_activity_shared_file_cannot_be_updated); + } } } else { // Detect Failure (403) --> maybe needs password @@ -918,14 +978,12 @@ public abstract class FileActivity extends DrawerActivity * @param share * @param screenTypePermission * @param isReshareShown - * @param isExpiryDateShown */ @Override - public void editExistingShare(OCShare share, int screenTypePermission, boolean isReshareShown, - boolean isExpiryDateShown) { + public void editExistingShare(OCShare share, int screenTypePermission, boolean isReshareShown) { FileDetailFragment fragment = getFileDetailFragment(); if (fragment != null) { - fragment.editExistingShare(share, screenTypePermission, isReshareShown, isExpiryDateShown); + fragment.editExistingShare(share, screenTypePermission, isReshareShown); } } @@ -947,4 +1005,23 @@ public abstract class FileActivity extends DrawerActivity } return null; } + + public FilesRepository getFilesRepository() { + return filesRepository; + } + + public void showSyncLoadingDialog(boolean isFolder) { + if (isFolder) { + return; + } + + showLoadingDialog(getApplicationContext().getString(R.string.wait_a_moment)); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void handleSyncDialogEvent(DialogEvent event) { + if (event.getType() == DialogEventType.SYNC) { + dismissLoadingDialog(); + } + } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java deleted file mode 100644 index 3fc80bd..0000000 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java +++ /dev/null @@ -1,2504 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2023 TSI-mc - * SPDX-FileCopyrightText: 2023 Archontis E. Kostis - * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-FileCopyrightText: 2018-2022 Tobias Kaminsky - * SPDX-FileCopyrightText: 2018-2020 Andy Scherzinger - * SPDX-FileCopyrightText: 2016 ownCloud Inc. - * SPDX-FileCopyrightText: 2012-2013 David A. Velasco - * SPDX-FileCopyrightText: 2011 Bartosz Przybylski - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later - */ -package com.owncloud.android.ui.activity; - -import android.accounts.Account; -import android.accounts.AuthenticatorException; -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.Dialog; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.ServiceConnection; -import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.content.res.Resources.NotFoundException; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.Parcelable; -import android.text.TextUtils; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewTreeObserver; -import android.view.WindowManager; - -import com.google.android.material.appbar.AppBarLayout; -import com.google.android.material.snackbar.Snackbar; -import com.nextcloud.appReview.InAppReviewHelper; -import com.nextcloud.client.account.User; -import com.nextcloud.client.appinfo.AppInfo; -import com.nextcloud.client.core.AsyncRunner; -import com.nextcloud.client.di.Injectable; -import com.nextcloud.client.editimage.EditImageActivity; -import com.nextcloud.client.files.DeepLinkHandler; -import com.nextcloud.client.jobs.download.FileDownloadHelper; -import com.nextcloud.client.jobs.download.FileDownloadWorker; -import com.nextcloud.client.jobs.upload.FileUploadHelper; -import com.nextcloud.client.jobs.upload.FileUploadWorker; -import com.nextcloud.client.media.PlayerServiceConnection; -import com.nextcloud.client.network.ClientFactory; -import com.nextcloud.client.network.ConnectivityService; -import com.nextcloud.client.preferences.AppPreferences; -import com.nextcloud.client.utils.IntentUtil; -import com.nextcloud.model.WorkerState; -import com.nextcloud.model.WorkerStateLiveData; -import com.nextcloud.utils.extensions.ActivityExtensionsKt; -import com.nextcloud.utils.extensions.BundleExtensionsKt; -import com.nextcloud.utils.extensions.IntentExtensionsKt; -import com.nextcloud.utils.view.FastScrollUtils; -import com.owncloud.android.MainApp; -import com.owncloud.android.R; -import com.owncloud.android.databinding.FilesBinding; -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.datamodel.VirtualFolderType; -import com.owncloud.android.files.services.NameCollisionPolicy; -import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.operations.RemoteOperation; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.files.RestoreFileVersionRemoteOperation; -import com.owncloud.android.lib.resources.files.SearchRemoteOperation; -import com.owncloud.android.operations.CopyFileOperation; -import com.owncloud.android.operations.CreateFolderOperation; -import com.owncloud.android.operations.DownloadType; -import com.owncloud.android.operations.MoveFileOperation; -import com.owncloud.android.operations.RefreshFolderOperation; -import com.owncloud.android.operations.RemoveFileOperation; -import com.owncloud.android.operations.RenameFileOperation; -import com.owncloud.android.operations.SynchronizeFileOperation; -import com.owncloud.android.operations.UploadFileOperation; -import com.owncloud.android.syncadapter.FileSyncAdapter; -import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask; -import com.owncloud.android.ui.asynctasks.FetchRemoteFileTask; -import com.owncloud.android.ui.asynctasks.GetRemoteFileTask; -import com.owncloud.android.ui.dialog.SendShareDialog; -import com.owncloud.android.ui.dialog.SortingOrderDialogFragment; -import com.owncloud.android.ui.dialog.StoragePermissionDialogFragment; -import com.owncloud.android.ui.events.SearchEvent; -import com.owncloud.android.ui.events.SyncEventFinished; -import com.owncloud.android.ui.events.TokenPushEvent; -import com.owncloud.android.ui.fragment.FileDetailFragment; -import com.owncloud.android.ui.fragment.FileFragment; -import com.owncloud.android.ui.fragment.GalleryFragment; -import com.owncloud.android.ui.fragment.GroupfolderListFragment; -import com.owncloud.android.ui.fragment.OCFileListFragment; -import com.owncloud.android.ui.fragment.SearchType; -import com.owncloud.android.ui.fragment.SharedListFragment; -import com.owncloud.android.ui.fragment.TaskRetainerFragment; -import com.owncloud.android.ui.fragment.UnifiedSearchFragment; -import com.owncloud.android.ui.helpers.FileOperationsHelper; -import com.owncloud.android.ui.helpers.UriUploader; -import com.owncloud.android.ui.preview.PreviewImageActivity; -import com.owncloud.android.ui.preview.PreviewImageFragment; -import com.owncloud.android.ui.preview.PreviewMediaActivity; -import com.owncloud.android.ui.preview.PreviewMediaFragment; -import com.owncloud.android.ui.preview.PreviewTextFileFragment; -import com.owncloud.android.ui.preview.PreviewTextFragment; -import com.owncloud.android.ui.preview.PreviewTextStringFragment; -import com.owncloud.android.ui.preview.pdf.PreviewPdfFragment; -import com.owncloud.android.utils.DataHolderUtil; -import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.ErrorMessageAdapter; -import com.owncloud.android.utils.FileSortOrder; -import com.owncloud.android.utils.MimeTypeUtil; -import com.owncloud.android.utils.PermissionUtil; -import com.owncloud.android.utils.PushUtils; -import com.owncloud.android.utils.StringUtils; -import com.owncloud.android.utils.theme.CapabilityUtils; - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Optional; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.SearchView; -import androidx.core.view.MenuItemCompat; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentTransaction; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import kotlin.Unit; - -import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR; -import static com.owncloud.android.utils.PermissionUtil.PERMISSION_CHOICE_DIALOG_TAG; - -/** - * Displays, what files the user has available in his Nextcloud. This is the main view. - */ -public class FileDisplayActivity extends FileActivity - implements FileFragment.ContainerActivity, - OnEnforceableRefreshListener, SortingOrderDialogFragment.OnSortingOrderListener, - SendShareDialog.SendShareDialogDownloader, Injectable { - - public static final String RESTART = "RESTART"; - public static final String ALL_FILES = "ALL_FILES"; - public static final String LIST_GROUPFOLDERS = "LIST_GROUPFOLDERS"; - public static final int SINGLE_USER_SIZE = 1; - public static final String OPEN_FILE = "NC_OPEN_FILE"; - - private FilesBinding binding; - - private SyncBroadcastReceiver mSyncBroadcastReceiver; - private UploadFinishReceiver mUploadFinishReceiver; - private DownloadFinishReceiver mDownloadFinishReceiver; - private RemoteOperationResult mLastSslUntrustedServerResult; - @Inject LocalBroadcastManager localBroadcastManager; - - public static final String TAG_PUBLIC_LINK = "PUBLIC_LINK"; - public static final String FTAG_CHOOSER_DIALOG = "CHOOSER_DIALOG"; - public static final String KEY_FILE_ID = "KEY_FILE_ID"; - public static final String KEY_FILE_PATH = "KEY_FILE_PATH"; - public static final String KEY_ACCOUNT = "KEY_ACCOUNT"; - public static final String KEY_IS_SORT_GROUP_VISIBLE = "KEY_IS_SORT_GROUP_VISIBLE"; - - private static final String KEY_WAITING_TO_PREVIEW = "WAITING_TO_PREVIEW"; - private static final String KEY_SYNC_IN_PROGRESS = "SYNC_IN_PROGRESS"; - private static final String KEY_WAITING_TO_SEND = "WAITING_TO_SEND"; - - public static final String ACTION_DETAILS = "com.owncloud.android.ui.activity.action.DETAILS"; - - public static final String DRAWER_MENU_ID = "DRAWER_MENU_ID"; - - public static final int REQUEST_CODE__SELECT_CONTENT_FROM_APPS = REQUEST_CODE__LAST_SHARED + 1; - public static final int REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM = REQUEST_CODE__LAST_SHARED + 2; - public static final int REQUEST_CODE__MOVE_OR_COPY_FILES = REQUEST_CODE__LAST_SHARED + 3; - public static final int REQUEST_CODE__UPLOAD_FROM_CAMERA = REQUEST_CODE__LAST_SHARED + 5; - - protected static final long DELAY_TO_REQUEST_REFRESH_OPERATION_LATER = DELAY_TO_REQUEST_OPERATIONS_LATER + 350; - - private static final String TAG = FileDisplayActivity.class.getSimpleName(); - - public static final String TAG_LIST_OF_FILES = "LIST_OF_FILES"; - - public static final String TEXT_PREVIEW = "TEXT_PREVIEW"; - - private OCFile mWaitingToPreview; - - private boolean mSyncInProgress; - - private OCFile mWaitingToSend; - - private Collection mDrawerMenuItemstoShowHideList; - - public static final String KEY_IS_SEARCH_OPEN = "IS_SEARCH_OPEN"; - public static final String KEY_SEARCH_QUERY = "SEARCH_QUERY"; - - public static final String REFRESH_FOLDER_EVENT_RECEIVER = "REFRESH_FOLDER_EVENT"; - - private String searchQuery = ""; - private boolean searchOpen; - - private SearchView searchView; - private PlayerServiceConnection mPlayerConnection; - private Optional lastDisplayedUser = Optional.empty(); - private int menuItemId = -1; - - @Inject AppPreferences preferences; - - @Inject AppInfo appInfo; - - @Inject ConnectivityService connectivityService; - - @Inject InAppReviewHelper inAppReviewHelper; - - @Inject FastScrollUtils fastScrollUtils; - @Inject AsyncRunner asyncRunner; - - public static Intent openFileIntent(Context context, User user, OCFile file) { - final Intent intent = new Intent(context, PreviewImageActivity.class); - intent.putExtra(FileActivity.EXTRA_FILE, file); - intent.putExtra(FileActivity.EXTRA_USER, user); - return intent; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - Log_OC.v(TAG, "onCreate() start"); - // Set the default theme to replace the launch screen theme. - setTheme(R.style.Theme_ownCloud_Toolbar_Drawer); - - super.onCreate(savedInstanceState); - loadSavedInstanceState(savedInstanceState); - - /// USER INTERFACE - initLayout(); - initUI(); - initTaskRetainerFragment(); - - // Restoring after UI has been inflated. - if (savedInstanceState != null) { - showSortListGroup(savedInstanceState.getBoolean(KEY_IS_SORT_GROUP_VISIBLE)); - } - - if (Intent.ACTION_VIEW.equals(getIntent().getAction())) { - handleOpenFileViaIntent(getIntent()); - } - - mPlayerConnection = new PlayerServiceConnection(this); - - checkStoragePath(); - - initSyncBroadcastReceiver(); - observeWorkerState(); - registerRefreshFolderEventReceiver(); - } - - @SuppressWarnings("unchecked") - private void loadSavedInstanceState(Bundle savedInstanceState) { - if (savedInstanceState != null) { - mWaitingToPreview = BundleExtensionsKt.getParcelableArgument(savedInstanceState, KEY_WAITING_TO_PREVIEW, OCFile.class); - mSyncInProgress = savedInstanceState.getBoolean(KEY_SYNC_IN_PROGRESS); - mWaitingToSend = BundleExtensionsKt.getParcelableArgument(savedInstanceState, KEY_WAITING_TO_SEND, OCFile.class); - searchQuery = savedInstanceState.getString(KEY_SEARCH_QUERY); - searchOpen = savedInstanceState.getBoolean(FileDisplayActivity.KEY_IS_SEARCH_OPEN, false); - } else { - mWaitingToPreview = null; - mSyncInProgress = false; - mWaitingToSend = null; - } - } - - private void initLayout() { - // Inflate and set the layout view - binding = FilesBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - } - - private void initUI() { - setupHomeSearchToolbarWithSortAndListButtons(); - mMenuButton.setOnClickListener(v -> openDrawer()); - mSwitchAccountButton.setOnClickListener(v -> showManageAccountsDialog()); - fastScrollUtils.fixAppBarForFastScroll(binding.appbar.appbar, binding.rootLayout); - } - - private void initTaskRetainerFragment() { - // Init Fragment without UI to retain AsyncTask across configuration changes - FragmentManager fm = getSupportFragmentManager(); - TaskRetainerFragment taskRetainerFragment = (TaskRetainerFragment) fm.findFragmentByTag(TaskRetainerFragment.FTAG_TASK_RETAINER_FRAGMENT); - if (taskRetainerFragment == null) { - taskRetainerFragment = new TaskRetainerFragment(); - fm.beginTransaction().add(taskRetainerFragment, TaskRetainerFragment.FTAG_TASK_RETAINER_FRAGMENT).commit(); - } // else, Fragment already created and retained across configuration change - } - - private void checkStoragePath() { - String newStorage = Environment.getExternalStorageDirectory().getAbsolutePath(); - String storagePath = preferences.getStoragePath(newStorage); - if (!preferences.isStoragePathValid() && !new File(storagePath).exists()) { - // falling back to default - preferences.setStoragePath(newStorage); - preferences.setStoragePathValid(); - MainApp.setStoragePath(newStorage); - - try { - AlertDialog alertDialog = new AlertDialog.Builder(this, R.style.Theme_ownCloud_Dialog).setTitle(R.string.wrong_storage_path).setMessage(R.string.wrong_storage_path_desc).setPositiveButton(R.string.dialog_close, (dialog, which) -> dialog.dismiss()).setIcon(R.drawable.ic_settings).create(); - - alertDialog.show(); - viewThemeUtils.platform.colorTextButtons(alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)); - } catch (WindowManager.BadTokenException e) { - Log_OC.e(TAG, "Error showing wrong storage info, so skipping it: " + e.getMessage()); - } - } - } - - @Override - public void onConfigurationChanged(@NonNull Configuration newConfig) { - super.onConfigurationChanged(newConfig); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - StoragePermissionDialogFragment fragment = (StoragePermissionDialogFragment) getSupportFragmentManager().findFragmentByTag(PERMISSION_CHOICE_DIALOG_TAG); - if (fragment != null) { - Dialog dialog = fragment.getDialog(); - - if (dialog != null && dialog.isShowing()) { - dialog.dismiss(); - getSupportFragmentManager().beginTransaction().remove(fragment).commitNowAllowingStateLoss(); - PermissionUtil.requestExternalStoragePermission(this, viewThemeUtils); - } - } - } - } - - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - - // handle notification permission on API level >= 33 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - // request notification permission first and then prompt for storage permissions - // storage permissions handled in onRequestPermissionsResult - PermissionUtil.requestNotificationPermission(this); - } else { - PermissionUtil.requestExternalStoragePermission(this, viewThemeUtils); - } - - if (IntentExtensionsKt.getParcelableArgument(getIntent(), OCFileListFragment.SEARCH_EVENT, SearchEvent.class) != null) { - switchToSearchFragment(savedInstanceState); - - int menuId = getIntent().getIntExtra(DRAWER_MENU_ID, -1); - if (menuId != -1) { - setupDrawer(menuId); - } - } else { - createMinFragments(savedInstanceState); - syncAndUpdateFolder(true); - } - - if (OPEN_FILE.equals(getIntent().getAction())) { - getSupportFragmentManager().executePendingTransactions(); - onOpenFileIntent(getIntent()); - } else if (RESTART.equals(getIntent().getAction())) { - // most likely switched to different account - DisplayUtils.showSnackMessage(this, String.format(getString(R.string.logged_in_as), accountManager.getUser().getAccountName())); - } - - upgradeNotificationForInstantUpload(); - checkOutdatedServer(); - } - - private Activity getActivity() { - return this; - } - - /** - * For Android 7+. Opens a pop up info for the new instant upload and disabled the old instant upload. - */ - private void upgradeNotificationForInstantUpload() { - // check for Android 6+ if legacy instant upload is activated --> disable + show info - if (preferences.instantPictureUploadEnabled() || preferences.instantVideoUploadEnabled()) { - preferences.removeLegacyPreferences(); - // show info pop-up - new AlertDialog.Builder(this, R.style.Theme_ownCloud_Dialog).setTitle(R.string.drawer_synced_folders).setMessage(R.string.synced_folders_new_info).setPositiveButton(R.string.drawer_open, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - // show instant upload - Intent syncedFoldersIntent = new Intent(getApplicationContext(), SyncedFoldersActivity.class); - dialog.dismiss(); - startActivity(syncedFoldersIntent); - } - }).setNegativeButton(R.string.drawer_close, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - } - }).setIcon(R.drawable.nav_synced_folders).show(); - } - } - - private void checkOutdatedServer() { - Optional user = getUser(); - // show outdated warning - if (user.isPresent() && CapabilityUtils.checkOutdatedWarning(getResources(), user.get().getServer().getVersion(), getCapabilities().getExtendedSupport().isTrue())) { - DisplayUtils.showServerOutdatedSnackbar(this, Snackbar.LENGTH_LONG); - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - switch (requestCode) { - case PermissionUtil.PERMISSIONS_POST_NOTIFICATIONS: - // handle notification permission on API level >= 33 - // dialogue was dismissed -> prompt for storage permissions - PermissionUtil.requestExternalStoragePermission(this, viewThemeUtils); - break; - case PermissionUtil.PERMISSIONS_EXTERNAL_STORAGE: - // If request is cancelled, result arrays are empty. - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - // permission was granted - EventBus.getDefault().post(new TokenPushEvent()); - syncAndUpdateFolder(true); - // toggle on is save since this is the only scenario this code gets accessed - } - break; - case PermissionUtil.PERMISSIONS_CAMERA: - // If request is cancelled, result arrays are empty. - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - // permission was granted - getFileOperationsHelper().uploadFromCamera(this, FileDisplayActivity.REQUEST_CODE__UPLOAD_FROM_CAMERA); - } - break; - default: - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - } - } - - private void switchToSearchFragment(Bundle savedInstanceState) { - if (savedInstanceState == null) { - OCFileListFragment listOfFiles = new OCFileListFragment(); - Bundle args = new Bundle(); - - args.putParcelable(OCFileListFragment.SEARCH_EVENT, - IntentExtensionsKt.getParcelableArgument(getIntent(), - OCFileListFragment.SEARCH_EVENT, - SearchEvent.class)); - args.putBoolean(OCFileListFragment.ARG_ALLOW_CONTEXTUAL_ACTIONS, true); - - listOfFiles.setArguments(args); - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.add(R.id.left_fragment_container, listOfFiles, TAG_LIST_OF_FILES); - transaction.commit(); - } else { - getSupportFragmentManager().findFragmentByTag(TAG_LIST_OF_FILES); - } - } - - private void createMinFragments(Bundle savedInstanceState) { - if (savedInstanceState == null) { - OCFileListFragment listOfFiles = new OCFileListFragment(); - Bundle args = new Bundle(); - args.putBoolean(OCFileListFragment.ARG_ALLOW_CONTEXTUAL_ACTIONS, true); - listOfFiles.setArguments(args); - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - transaction.add(R.id.left_fragment_container, listOfFiles, TAG_LIST_OF_FILES); - transaction.commit(); - } else { - getSupportFragmentManager().findFragmentByTag(TAG_LIST_OF_FILES); - } - } - - private void initFragments() { - /// First fragment - OCFileListFragment listOfFiles = getListOfFilesFragment(); - if (listOfFiles != null && TextUtils.isEmpty(searchQuery)) { - listOfFiles.listDirectory(getCurrentDir(), getFile(), MainApp.isOnlyOnDevice(), false); - } else { - Log_OC.e(TAG, "Still have a chance to lose the initialization of list fragment >("); - } - - /// reset views - resetTitleBarAndScrolling(); - } - - // Is called with the flag FLAG_ACTIVITY_SINGLE_TOP and set the new file and intent - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - setIntent(intent); - - if (ACTION_DETAILS.equalsIgnoreCase(intent.getAction())) { - OCFile file = IntentExtensionsKt.getParcelableArgument(intent, EXTRA_FILE, OCFile.class); - setFile(file); - setIntent(intent); - showDetails(file); - } else if (Intent.ACTION_VIEW.equals(intent.getAction())) { - handleOpenFileViaIntent(intent); - } else if (OPEN_FILE.equals(intent.getAction())) { - onOpenFileIntent(intent); - } else if (RESTART.equals(intent.getAction())) { - finish(); - startActivity(intent); - } else // Verify the action and get the query - if (Intent.ACTION_SEARCH.equals(intent.getAction())) { - setIntent(intent); - - SearchEvent searchEvent = IntentExtensionsKt.getParcelableArgument(intent, OCFileListFragment.SEARCH_EVENT, SearchEvent.class); - if (searchEvent != null) { - if (SearchRemoteOperation.SearchType.PHOTO_SEARCH == searchEvent.getSearchType()) { - Log_OC.d(this, "Switch to photo search fragment"); - - GalleryFragment photoFragment = new GalleryFragment(); - Bundle bundle = new Bundle(); - bundle.putParcelable(OCFileListFragment.SEARCH_EVENT, searchEvent); - photoFragment.setArguments(bundle); - setLeftFragment(photoFragment); - } else if (searchEvent.getSearchType() == SearchRemoteOperation.SearchType.SHARED_FILTER) { - Log_OC.d(this, "Switch to shared fragment"); - SharedListFragment sharedListFragment = new SharedListFragment(); - Bundle bundle = new Bundle(); - bundle.putParcelable(OCFileListFragment.SEARCH_EVENT, searchEvent); - sharedListFragment.setArguments(bundle); - setLeftFragment(sharedListFragment); - } else { - Log_OC.d(this, "Switch to oc file search fragment"); - - OCFileListFragment photoFragment = new OCFileListFragment(); - Bundle bundle = new Bundle(); - bundle.putParcelable(OCFileListFragment.SEARCH_EVENT, searchEvent); - photoFragment.setArguments(bundle); - setLeftFragment(photoFragment); - } - } - } else if (ALL_FILES.equals(intent.getAction())) { - Log_OC.d(this, "Switch to oc file fragment"); - - setLeftFragment(new OCFileListFragment()); - getSupportFragmentManager().executePendingTransactions(); - browseToRoot(); - } else if (LIST_GROUPFOLDERS.equals(intent.getAction())) { - Log_OC.d(this, "Switch to list groupfolders fragment"); - - setLeftFragment(new GroupfolderListFragment()); - getSupportFragmentManager().executePendingTransactions(); - } - } - - private void onOpenFileIntent(Intent intent) { - String extra = intent.getStringExtra(EXTRA_FILE); - OCFile file = getStorageManager().getFileByDecryptedRemotePath(extra); - if (file != null) { - OCFileListFragment fileFragment; - final Fragment leftFragment = getLeftFragment(); - if (leftFragment instanceof OCFileListFragment) { - fileFragment = (OCFileListFragment) leftFragment; - } else { - fileFragment = new OCFileListFragment(); - setLeftFragment(fileFragment); - } - fileFragment.onItemClicked(file); - } - } - - /** - * Replaces the first fragment managed by the activity with the received as a parameter. - * - * @param fragment New Fragment to set. - */ - private void setLeftFragment(Fragment fragment) { - setLeftFragment(fragment, true); - } - - private void setLeftFragment(Fragment fragment, boolean showSortListGroup) { - if (searchView != null) { - searchView.post(() -> searchView.setQuery(searchQuery, true)); - } - setDrawerIndicatorEnabled(false); - - //clear the subtitle while navigating to any other screen from Media screen - clearToolbarSubtitle(); - - showSortListGroup(showSortListGroup); - - FragmentManager fragmentManager = getSupportFragmentManager(); - if (!isFinishing() && !fragmentManager.isDestroyed()) { - FragmentTransaction transaction = fragmentManager.beginTransaction(); - transaction.addToBackStack(null); - transaction.replace(R.id.left_fragment_container, fragment, TAG_LIST_OF_FILES); - transaction.commit(); - } - } - - private OCFileListFragment getOCFileListFragmentFromFile() { - final Fragment leftFragment = getLeftFragment(); - OCFileListFragment listOfFiles; - - if (leftFragment instanceof OCFileListFragment) { - listOfFiles = (OCFileListFragment) leftFragment; - } else { - listOfFiles = new OCFileListFragment(); - Bundle args = new Bundle(); - args.putBoolean(OCFileListFragment.ARG_ALLOW_CONTEXTUAL_ACTIONS, true); - listOfFiles.setArguments(args); - - FragmentManager fm = getSupportFragmentManager(); - boolean isExecutingTransactions = !fm.isStateSaved() && !fm.executePendingTransactions(); - - if (isExecutingTransactions) { - setLeftFragment(listOfFiles); - fm.executePendingTransactions(); - } else { - new Handler(Looper.getMainLooper()).post(() -> { - setLeftFragment(listOfFiles); - fm.executePendingTransactions(); - }); - } - } - - return listOfFiles; - } - - - public void showFileActions(OCFile file) { - dismissLoadingDialog(); - OCFileListFragment listOfFiles = getOCFileListFragmentFromFile(); - browseUp(listOfFiles); - listOfFiles.onOverflowIconClicked(file, null); - } - - public @androidx.annotation.Nullable Fragment getLeftFragment() { - return getSupportFragmentManager().findFragmentByTag(FileDisplayActivity.TAG_LIST_OF_FILES); - } - - public @androidx.annotation.Nullable - @Deprecated OCFileListFragment getListOfFilesFragment() { - Fragment listOfFiles = getSupportFragmentManager().findFragmentByTag(FileDisplayActivity.TAG_LIST_OF_FILES); - if (listOfFiles instanceof OCFileListFragment) { - return (OCFileListFragment) listOfFiles; - } - Log_OC.e(TAG, "Access to unexisting list of files fragment!!"); - return null; - } - - protected void resetTitleBarAndScrolling() { - updateActionBarTitleAndHomeButton(null); - resetScrolling(true); - } - - public void updateListOfFilesFragment(boolean fromSearch) { - OCFileListFragment fileListFragment = getListOfFilesFragment(); - if (fileListFragment != null) { - fileListFragment.listDirectory(MainApp.isOnlyOnDevice(), fromSearch); - } - } - - public void resetSearchView() { - OCFileListFragment fileListFragment = getListOfFilesFragment(); - - if (fileListFragment != null) { - fileListFragment.setSearchFragment(false); - } - } - - protected void refreshDetailsFragmentIfVisible(String downloadEvent, String downloadedRemotePath, boolean success) { - Fragment leftFragment = getLeftFragment(); - if (leftFragment instanceof FileDetailFragment) { - boolean waitedPreview = mWaitingToPreview != null && mWaitingToPreview.getRemotePath().equals(downloadedRemotePath); - FileDetailFragment detailsFragment = (FileDetailFragment) leftFragment; - OCFile fileInFragment = detailsFragment.getFile(); - if (fileInFragment != null && !downloadedRemotePath.equals(fileInFragment.getRemotePath())) { - // the user browsed to other file ; forget the automatic preview - mWaitingToPreview = null; - - } else if (downloadEvent.equals(FileDownloadWorker.Companion.getDownloadAddedMessage())) { - // grant that the details fragment updates the progress bar - detailsFragment.listenForTransferProgress(); - detailsFragment.updateFileDetails(true, false); - - } else if (downloadEvent.equals(FileDownloadWorker.Companion.getDownloadFinishMessage())) { - // update the details panel - boolean detailsFragmentChanged = false; - if (waitedPreview) { - if (success) { - // update the file from database, for the local storage path - mWaitingToPreview = getStorageManager().getFileById(mWaitingToPreview.getFileId()); - - if (PreviewMediaActivity.Companion.canBePreviewed(mWaitingToPreview)) { - startMediaPreview(mWaitingToPreview, 0, true, true, true, true); - detailsFragmentChanged = true; - } else if (MimeTypeUtil.isVCard(mWaitingToPreview.getMimeType())) { - startContactListFragment(mWaitingToPreview); - detailsFragmentChanged = true; - } else if (PreviewTextFileFragment.canBePreviewed(mWaitingToPreview)) { - startTextPreview(mWaitingToPreview, true); - detailsFragmentChanged = true; - } else if (MimeTypeUtil.isPDF(mWaitingToPreview)) { - startPdfPreview(mWaitingToPreview); - detailsFragmentChanged = true; - } else { - getFileOperationsHelper().openFile(mWaitingToPreview); - } - } - mWaitingToPreview = null; - } - if (!detailsFragmentChanged) { - detailsFragment.updateFileDetails(false, success); - } - } - } - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - boolean drawerOpen = isDrawerOpen(); - - for (MenuItem menuItem : mDrawerMenuItemstoShowHideList) { - menuItem.setVisible(!drawerOpen); - } - - return super.onPrepareOptionsMenu(menu); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.activity_file_display, menu); - - menu.findItem(R.id.action_select_all).setVisible(false); - MenuItem searchMenuItem = menu.findItem(R.id.action_search); - searchView = (SearchView) MenuItemCompat.getActionView(searchMenuItem); - searchMenuItem.setVisible(false); - mSearchText.setOnClickListener(v -> { - showSearchView(); - searchView.setIconified(false); - }); - - viewThemeUtils.androidx.themeToolbarSearchView(searchView); - - // populate list of menu items to show/hide when drawer is opened/closed - mDrawerMenuItemstoShowHideList = new ArrayList<>(1); - mDrawerMenuItemstoShowHideList.add(searchMenuItem); - - //focus the SearchView - if (!TextUtils.isEmpty(searchQuery)) { - searchView.post(() -> { - searchView.setIconified(false); - searchView.setQuery(searchQuery, true); - searchView.clearFocus(); - }); - } - - final View mSearchEditFrame = searchView.findViewById(androidx.appcompat.R.id.search_edit_frame); - - searchView.setOnCloseListener(() -> { - if (TextUtils.isEmpty(searchView.getQuery().toString())) { - searchView.onActionViewCollapsed(); - setDrawerIndicatorEnabled(isDrawerIndicatorAvailable()); // order matters - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - mDrawerToggle.syncState(); - - OCFileListFragment ocFileListFragment = getListOfFilesFragment(); - if (ocFileListFragment != null) { - ocFileListFragment.setSearchFragment(false); - ocFileListFragment.refreshDirectory(); - } - } else { - searchView.post(() -> searchView.setQuery("", true)); - } - return true; - }); - - ViewTreeObserver vto = mSearchEditFrame.getViewTreeObserver(); - vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - int oldVisibility = -1; - - @Override - public void onGlobalLayout() { - - int currentVisibility = mSearchEditFrame.getVisibility(); - - if (currentVisibility != oldVisibility) { - if (currentVisibility == View.VISIBLE) { - setDrawerIndicatorEnabled(false); - } - - oldVisibility = currentVisibility; - } - - } - }); - - return super.onCreateOptionsMenu(menu); - } - - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - boolean retval = true; - - int itemId = item.getItemId(); - - if (itemId == android.R.id.home) { - if (!isDrawerOpen() && !isSearchOpen() && isRoot(getCurrentDir()) && getLeftFragment() instanceof OCFileListFragment) { - openDrawer(); - } else { - onBackPressed(); - } - } else if (itemId == R.id.action_select_all) { - OCFileListFragment fragment = getListOfFilesFragment(); - - if (fragment != null) { - fragment.selectAllFiles(true); - } - } else { - retval = super.onOptionsItemSelected(item); - } - - return retval; - } - - /** - * Called, when the user selected something for uploading - */ - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == REQUEST_CODE__SELECT_CONTENT_FROM_APPS && (resultCode == RESULT_OK || resultCode == UploadFilesActivity.RESULT_OK_AND_MOVE)) { - - requestUploadOfContentFromApps(data, resultCode); - - } else if (requestCode == REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM && (resultCode == RESULT_OK || resultCode == UploadFilesActivity.RESULT_OK_AND_MOVE || resultCode == UploadFilesActivity.RESULT_OK_AND_DO_NOTHING || resultCode == UploadFilesActivity.RESULT_OK_AND_DELETE)) { - - requestUploadOfFilesFromFileSystem(data, resultCode); - - } else if (requestCode == REQUEST_CODE__UPLOAD_FROM_CAMERA && (resultCode == RESULT_OK || resultCode == UploadFilesActivity.RESULT_OK_AND_DELETE)) { - - new CheckAvailableSpaceTask(new CheckAvailableSpaceTask.CheckAvailableSpaceListener() { - @Override - public void onCheckAvailableSpaceStart() { - Log_OC.d(this, "onCheckAvailableSpaceStart"); - } - - @Override - public void onCheckAvailableSpaceFinish(boolean hasEnoughSpaceAvailable, String... filesToUpload) { - Log_OC.d(this, "onCheckAvailableSpaceFinish"); - - if (hasEnoughSpaceAvailable) { - File file = new File(filesToUpload[0]); - File renamedFile = new File(file.getParent() + PATH_SEPARATOR + FileOperationsHelper.getCapturedImageName()); - - if (!file.renameTo(renamedFile)) { - DisplayUtils.showSnackMessage(getActivity(), "Fail to upload taken image!"); - return; - } - - requestUploadOfFilesFromFileSystem(renamedFile.getParentFile().getAbsolutePath(), new String[]{renamedFile.getAbsolutePath()}, FileUploadWorker.LOCAL_BEHAVIOUR_DELETE); - } - } - }, new String[]{FileOperationsHelper.createImageFile(getActivity()).getAbsolutePath()}).execute(); - } else if (requestCode == REQUEST_CODE__MOVE_OR_COPY_FILES && resultCode == RESULT_OK) { - exitSelectionMode(); - } else if (requestCode == PermissionUtil.REQUEST_CODE_MANAGE_ALL_FILES) { - syncAndUpdateFolder(true); - } else { - super.onActivityResult(requestCode, resultCode, data); - } - } - - private void exitSelectionMode() { - OCFileListFragment ocFileListFragment = getListOfFilesFragment(); - if (ocFileListFragment != null) { - ocFileListFragment.exitSelectionMode(); - } - } - - private void requestUploadOfFilesFromFileSystem(Intent data, int resultCode) { - String[] filePaths = data.getStringArrayExtra(UploadFilesActivity.EXTRA_CHOSEN_FILES); - String basePath = data.getStringExtra(UploadFilesActivity.LOCAL_BASE_PATH); - requestUploadOfFilesFromFileSystem(basePath, filePaths, resultCode); - } - - private String[] getRemotePaths(String directory, String[] filePaths, String localBasePath) { - String[] remotePaths = new String[filePaths.length]; - for (int j = 0; j < remotePaths.length; j++) { - String relativePath = StringUtils.removePrefix(filePaths[j], localBasePath); - remotePaths[j] = directory + relativePath; - } - - return remotePaths; - } - - private void requestUploadOfFilesFromFileSystem(String localBasePath, String[] filePaths, int resultCode) { - if (localBasePath != null && filePaths != null) { - if (!localBasePath.endsWith("/")) { - localBasePath = localBasePath + "/"; - } - - String remotePathBase = getCurrentDir().getRemotePath(); - String[] decryptedRemotePaths = getRemotePaths(remotePathBase, filePaths, localBasePath); - - int behaviour = switch (resultCode) { - case UploadFilesActivity.RESULT_OK_AND_MOVE -> FileUploadWorker.LOCAL_BEHAVIOUR_MOVE; - case UploadFilesActivity.RESULT_OK_AND_DELETE -> FileUploadWorker.LOCAL_BEHAVIOUR_DELETE; - default -> FileUploadWorker.LOCAL_BEHAVIOUR_FORGET; - }; - - FileUploadHelper.Companion.instance().uploadNewFiles(getUser().orElseThrow(RuntimeException::new), - filePaths, - decryptedRemotePaths, - behaviour, - true, - UploadFileOperation.CREATED_BY_USER, - false, - false, - NameCollisionPolicy.ASK_USER); - - } else { - Log_OC.d(TAG, "User clicked on 'Update' with no selection"); - DisplayUtils.showSnackMessage(this, R.string.filedisplay_no_file_selected); - } - } - - private void requestUploadOfContentFromApps(Intent contentIntent, int resultCode) { - - ArrayList streamsToUpload = new ArrayList<>(); - - if (contentIntent.getClipData() != null && contentIntent.getClipData().getItemCount() > 0) { - - for (int i = 0; i < contentIntent.getClipData().getItemCount(); i++) { - streamsToUpload.add(contentIntent.getClipData().getItemAt(i).getUri()); - } - - } else { - streamsToUpload.add(contentIntent.getData()); - } - - int behaviour = (resultCode == UploadFilesActivity.RESULT_OK_AND_MOVE) ? FileUploadWorker.LOCAL_BEHAVIOUR_MOVE : FileUploadWorker.LOCAL_BEHAVIOUR_COPY; - - OCFile currentDir = getCurrentDir(); - String remotePath = (currentDir != null) ? currentDir.getRemotePath() : OCFile.ROOT_PATH; - - UriUploader uploader = new UriUploader(this, streamsToUpload, remotePath, getUser().orElseThrow(RuntimeException::new), behaviour, false, // Not show waiting dialog while file is being copied from private storage - null // Not needed copy temp task listener - ); - - uploader.uploadUris(); - - } - - private boolean isSearchOpen() { - if (searchView == null) { - return false; - } else { - View mSearchEditFrame = searchView.findViewById(androidx.appcompat.R.id.search_edit_frame); - return mSearchEditFrame != null && mSearchEditFrame.getVisibility() == View.VISIBLE; - } - } - - private Boolean isRootDirectory() { - OCFile currentDir = getCurrentDir(); - return (currentDir == null || currentDir.getParentId() == FileDataStorageManager.ROOT_PARENT_ID); - } - - /* - * BackPressed priority/hierarchy: - * 1. close search view if opened - * 2. close drawer if opened - * 3. if it is OCFileListFragment and it's in Root -> (finish Activity) or it's not Root -> (browse up) - * 4. otherwise pop up the fragment and sortGroup view visibility and call super.onBackPressed() - */ - @SuppressFBWarnings("ITC_INHERITANCE_TYPE_CHECKING") - @Override - public void onBackPressed() { - final boolean isDrawerOpen = isDrawerOpen(); - final boolean isSearchOpen = isSearchOpen(); - - final Fragment leftFragment = getLeftFragment(); - - if (isSearchOpen) { - resetSearchAction(); - } else if (isDrawerOpen) { - super.onBackPressed(); - } else if (leftFragment instanceof OCFileListFragment listOfFiles) { - - // all closed - OCFile currentDir = getCurrentDir(); - if (isRoot(currentDir)) { - finish(); - return; - } - browseUp(listOfFiles); - } else { - popBack(); - } - } - - private void browseUp(OCFileListFragment listOfFiles) { - listOfFiles.onBrowseUp(); - setFile(listOfFiles.getCurrentFile()); - listOfFiles.setFabVisible(true); - listOfFiles.registerFabListener(); - resetTitleBarAndScrolling(); - setDrawerAllFiles(); - } - - private void resetSearchAction() { - Fragment leftFragment = getLeftFragment(); - if (isSearchOpen() && searchView != null) { - searchView.setQuery("", true); - searchView.onActionViewCollapsed(); - searchView.clearFocus(); - - if (isRoot(getCurrentDir()) && leftFragment instanceof OCFileListFragment listOfFiles) { - - // Remove the list to the original state - ArrayList listOfHiddenFiles = listOfFiles.getAdapter().listOfHiddenFiles; - listOfFiles.performSearch("", listOfHiddenFiles, true); - - hideSearchView(getCurrentDir()); - setDrawerIndicatorEnabled(isDrawerIndicatorAvailable()); - } - if (leftFragment instanceof UnifiedSearchFragment) { - showSortListGroup(false); - super.onBackPressed(); - } - } - } - - /** - * Use this method when want to pop the fragment on back press. It resets Scrolling (See - * {@link #resetScrolling(boolean) with true} and pop the visibility for sortListGroup (See - * {@link #showSortListGroup(boolean) with false}. At last call to super.onBackPressed() - */ - private void popBack() { - binding.fabMain.setImageResource(R.drawable.ic_plus); - resetScrolling(true); - showSortListGroup(false); - super.onBackPressed(); - } - - @Override - protected void onSaveInstanceState(@NonNull Bundle outState) { - // responsibility of restore is preferred in onCreate() before than in - // onRestoreInstanceState when there are Fragments involved - Log_OC.v(TAG, "onSaveInstanceState() start"); - super.onSaveInstanceState(outState); - outState.putParcelable(FileDisplayActivity.KEY_WAITING_TO_PREVIEW, mWaitingToPreview); - outState.putBoolean(FileDisplayActivity.KEY_SYNC_IN_PROGRESS, mSyncInProgress); - // outState.putBoolean(FileDisplayActivity.KEY_REFRESH_SHARES_IN_PROGRESS, - // mRefreshSharesInProgress); - outState.putParcelable(FileDisplayActivity.KEY_WAITING_TO_SEND, mWaitingToSend); - if (searchView != null) { - outState.putBoolean(KEY_IS_SEARCH_OPEN, !searchView.isIconified()); - } - outState.putString(KEY_SEARCH_QUERY, searchQuery); - outState.putBoolean(KEY_IS_SORT_GROUP_VISIBLE, sortListGroupVisibility()); - Log_OC.v(TAG, "onSaveInstanceState() end"); - } - - @Override - protected void onResume() { - Log_OC.v(TAG, "onResume() start"); - super.onResume(); - // Instead of onPostCreate, starting the loading in onResume for children fragments - Fragment leftFragment = getLeftFragment(); - - // Listen for sync messages - if (!(leftFragment instanceof OCFileListFragment) || !((OCFileListFragment) leftFragment).isSearchFragment()) { - initSyncBroadcastReceiver(); - } - - if (!(leftFragment instanceof OCFileListFragment)) { - if (leftFragment instanceof FileFragment) { - super.updateActionBarTitleAndHomeButton(((FileFragment) leftFragment).getFile()); - } - return; - } - - OCFileListFragment ocFileListFragment = (OCFileListFragment) leftFragment; - - ocFileListFragment.setLoading(mSyncInProgress); - syncAndUpdateFolder(false, true); - - OCFile startFile = null; - if (getIntent() != null) { - OCFile fileArgs = IntentExtensionsKt.getParcelableArgument(getIntent(), EXTRA_FILE, OCFile.class); - if (fileArgs != null) { - startFile = fileArgs; - setFile(startFile); - } - } - - // refresh list of files - if (searchView != null && !TextUtils.isEmpty(searchQuery)) { - searchView.setQuery(searchQuery, false); - } else if (!ocFileListFragment.isSearchFragment() && startFile == null) { - updateListOfFilesFragment(false); - ocFileListFragment.registerFabListener(); - } else { - ocFileListFragment.listDirectory(startFile, false, false); - updateActionBarTitleAndHomeButton(startFile); - } - - // Listen for upload messages - IntentFilter uploadIntentFilter = new IntentFilter(FileUploadWorker.Companion.getUploadFinishMessage()); - mUploadFinishReceiver = new UploadFinishReceiver(); - localBroadcastManager.registerReceiver(mUploadFinishReceiver, uploadIntentFilter); - - // Listen for download messages - IntentFilter downloadIntentFilter = new IntentFilter(FileDownloadWorker.Companion.getDownloadAddedMessage()); - downloadIntentFilter.addAction(FileDownloadWorker.Companion.getDownloadFinishMessage()); - mDownloadFinishReceiver = new DownloadFinishReceiver(); - localBroadcastManager.registerReceiver(mDownloadFinishReceiver, downloadIntentFilter); - - // setup drawer - menuItemId = getIntent().getIntExtra(FileDisplayActivity.DRAWER_MENU_ID, -1); - - if (menuItemId == -1) { - setDrawerAllFiles(); - } else { - if (menuItemId == R.id.nav_all_files || menuItemId == R.id.nav_personal_files) { - setupHomeSearchToolbarWithSortAndListButtons(); - } else { - setupToolbar(); - } - setDrawerMenuItemChecked(menuItemId); - } - - if (ocFileListFragment instanceof GalleryFragment) { - updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_item_gallery)); - } - //show in-app review dialog to user - inAppReviewHelper.showInAppReview(this); - - Log_OC.v(TAG, "onResume() end"); - } - - private void setDrawerAllFiles() { - if (MainApp.isOnlyPersonFiles()) { - setDrawerMenuItemChecked(R.id.nav_personal_files); - setupHomeSearchToolbarWithSortAndListButtons(); - } else if (MainApp.isOnlyOnDevice()) { - setDrawerMenuItemChecked(R.id.nav_on_device); - setupToolbar(); - } else { - int lastMenuItem = getCheckedMenuItem(); - if (lastMenuItem == Menu.NONE) { - lastMenuItem = R.id.nav_all_files; - } - - setDrawerMenuItemChecked(lastMenuItem); - setupHomeSearchToolbarWithSortAndListButtons(); - } - } - - public void initSyncBroadcastReceiver() { - if (mSyncBroadcastReceiver == null) { - IntentFilter syncIntentFilter = new IntentFilter(FileSyncAdapter.EVENT_FULL_SYNC_START); - syncIntentFilter.addAction(FileSyncAdapter.EVENT_FULL_SYNC_END); - syncIntentFilter.addAction(FileSyncAdapter.EVENT_FULL_SYNC_FOLDER_CONTENTS_SYNCED); - syncIntentFilter.addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED); - syncIntentFilter.addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED); - mSyncBroadcastReceiver = new SyncBroadcastReceiver(); - localBroadcastManager.registerReceiver(mSyncBroadcastReceiver, syncIntentFilter); - } - } - - @Override - protected void onPause() { - Log_OC.v(TAG, "onPause() start"); - if (mSyncBroadcastReceiver != null) { - localBroadcastManager.unregisterReceiver(mSyncBroadcastReceiver); - mSyncBroadcastReceiver = null; - } - if (mUploadFinishReceiver != null) { - localBroadcastManager.unregisterReceiver(mUploadFinishReceiver); - mUploadFinishReceiver = null; - } - if (mDownloadFinishReceiver != null) { - localBroadcastManager.unregisterReceiver(mDownloadFinishReceiver); - mDownloadFinishReceiver = null; - } - - super.onPause(); - Log_OC.v(TAG, "onPause() end"); - } - - @Override - public void onSortingOrderChosen(FileSortOrder selection) { - OCFileListFragment ocFileListFragment = getListOfFilesFragment(); - if (ocFileListFragment != null) { - ocFileListFragment.sortFiles(selection); - } - } - - @Override - public void downloadFile(OCFile file, String packageName, String activityName) { - startDownloadForSending(file, OCFileListFragment.DOWNLOAD_SEND, packageName, activityName); - } - - private class SyncBroadcastReceiver extends BroadcastReceiver { - - /** - * {@link BroadcastReceiver} to enable syncing feedback in UI - */ - @SuppressLint("VisibleForTests") - @Override - public void onReceive(Context context, Intent intent) { - try { - String event = intent.getAction(); - Log_OC.d(TAG, "Received broadcast " + event); - String accountName = intent.getStringExtra(FileSyncAdapter.EXTRA_ACCOUNT_NAME); - - String synchFolderRemotePath = intent.getStringExtra(FileSyncAdapter.EXTRA_FOLDER_PATH); - RemoteOperationResult synchResult = (RemoteOperationResult) DataHolderUtil.getInstance().retrieve(intent.getStringExtra(FileSyncAdapter.EXTRA_RESULT)); - boolean sameAccount = getAccount() != null && accountName.equals(getAccount().name) && getStorageManager() != null; - - if (sameAccount) { - - if (FileSyncAdapter.EVENT_FULL_SYNC_START.equals(event)) { - mSyncInProgress = true; - - } else { - OCFile currentFile = (getFile() == null) ? null : getStorageManager().getFileByPath(getFile().getRemotePath()); - OCFile currentDir = (getCurrentDir() == null) ? null : getStorageManager().getFileByPath(getCurrentDir().getRemotePath()); - - if (currentDir == null) { - // current folder was removed from the server - DisplayUtils.showSnackMessage(getActivity(), R.string.sync_current_folder_was_removed, synchFolderRemotePath); - - browseToRoot(); - - } else { - if (currentFile == null && !getFile().isFolder()) { - // currently selected file was removed in the server, and now we - // know it - resetTitleBarAndScrolling(); - currentFile = currentDir; - } - - if (currentDir.getRemotePath().equals(synchFolderRemotePath)) { - OCFileListFragment fileListFragment = getListOfFilesFragment(); - if (fileListFragment != null) { - fileListFragment.listDirectory(currentDir, MainApp.isOnlyOnDevice(), false); - } - } - setFile(currentFile); - } - - mSyncInProgress = !FileSyncAdapter.EVENT_FULL_SYNC_END.equals(event) && !RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED.equals(event); - - if (RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED.equals(event) && synchResult != null) { - - if (synchResult.isSuccess()) { - hideInfoBox(); - } else { - // TODO refactor and make common - if (checkForRemoteOperationError(synchResult)) { - requestCredentialsUpdate(context); - } else { - switch (synchResult.getCode()) { - case SSL_RECOVERABLE_PEER_UNVERIFIED: - showUntrustedCertDialog(synchResult); - break; - - case MAINTENANCE_MODE: - showInfoBox(R.string.maintenance_mode); - break; - - case NO_NETWORK_CONNECTION: - showInfoBox(R.string.offline_mode); - break; - - case HOST_NOT_AVAILABLE: - showInfoBox(R.string.host_not_available); - break; - - default: - // nothing to do - break; - } - } - } - } - DataHolderUtil.getInstance().delete(intent.getStringExtra(FileSyncAdapter.EXTRA_RESULT)); - - Log_OC.d(TAG, "Setting progress visibility to " + mSyncInProgress); - - - OCFileListFragment ocFileListFragment = getListOfFilesFragment(); - if (ocFileListFragment != null) { - ocFileListFragment.setLoading(mSyncInProgress); - if (!mSyncInProgress && !ocFileListFragment.isLoading()) { - // update scrolling when load finishes - if (ocFileListFragment.isEmpty()) { - lockScrolling(); - } else { - resetScrolling(false); - } - } - } - setBackgroundText(); - } - } - - if (synchResult != null && synchResult.getCode() == ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED) { - mLastSslUntrustedServerResult = synchResult; - } - } catch (RuntimeException e) { - // avoid app crashes after changing the serial id of RemoteOperationResult - // in owncloud library with broadcast notifications pending to process - - try { - DataHolderUtil.getInstance().delete(intent.getStringExtra(FileSyncAdapter.EXTRA_RESULT)); - } catch (RuntimeException re) { - // we did not send this intent, so ignoring - Log_OC.i(TAG, "Ignoring error deleting data"); - } - } - } - } - - private boolean checkForRemoteOperationError(RemoteOperationResult syncResult) { - return ResultCode.UNAUTHORIZED == syncResult.getCode() || (syncResult.isException() && syncResult.getException() instanceof AuthenticatorException); - } - - /** - * Show a text message on screen view for notifying user if content is loading or folder is empty - */ - private void setBackgroundText() { - final OCFileListFragment ocFileListFragment = getListOfFilesFragment(); - if (ocFileListFragment != null) { - if (mSyncInProgress || getFile().getFileLength() > 0 && getStorageManager().getFolderContent(getFile(), false).isEmpty()) { - ocFileListFragment.setEmptyListLoadingMessage(); - } else { - if (MainApp.isOnlyOnDevice()) { - ocFileListFragment.setMessageForEmptyList(R.string.file_list_empty_headline, R.string.file_list_empty_on_device, R.drawable.ic_list_empty_folder, true); - } else { - ocFileListFragment.setEmptyListMessage(SearchType.NO_SEARCH); - } - } - } else { - Log_OC.e(TAG, "OCFileListFragment is null"); - } - } - - /** - * Once the file upload has finished -> update view - */ - private class UploadFinishReceiver extends BroadcastReceiver { - /** - * Once the file upload has finished -> update view - *

- * {@link BroadcastReceiver} to enable upload feedback in UI - */ - @Override - public void onReceive(Context context, Intent intent) { - String uploadedRemotePath = intent.getStringExtra(FileUploadWorker.EXTRA_REMOTE_PATH); - String accountName = intent.getStringExtra(FileUploadWorker.ACCOUNT_NAME); - Account account = getAccount(); - boolean sameAccount = accountName != null && account != null && accountName.equals(account.name); - OCFile currentDir = getCurrentDir(); - boolean isDescendant = currentDir != null && uploadedRemotePath != null && uploadedRemotePath.startsWith(currentDir.getRemotePath()); - - if (sameAccount && isDescendant) { - String linkedToRemotePath = intent.getStringExtra(FileUploadWorker.EXTRA_LINKED_TO_PATH); - if (linkedToRemotePath == null || isAscendant(linkedToRemotePath)) { - updateListOfFilesFragment(false); - } - } - - boolean uploadWasFine = intent.getBooleanExtra(FileUploadWorker.EXTRA_UPLOAD_RESULT, false); - boolean renamedInUpload = getFile().getRemotePath().equals(intent.getStringExtra(FileUploadWorker.EXTRA_OLD_REMOTE_PATH)); - - boolean sameFile = getFile().getRemotePath().equals(uploadedRemotePath) || renamedInUpload; - Fragment details = getLeftFragment(); - - if (sameAccount && sameFile && details instanceof FileDetailFragment) { - if (uploadWasFine) { - setFile(getStorageManager().getFileByPath(uploadedRemotePath)); - } else { - //TODO remove upload progress bar after upload failed. - Log_OC.d(TAG, "Remove upload progress bar after upload failed"); - } - if (renamedInUpload) { - String newName = new File(uploadedRemotePath).getName(); - DisplayUtils.showSnackMessage(getActivity(), R.string.filedetails_renamed_in_upload_msg, newName); - } - if (uploadWasFine || getFile().fileExists()) { - ((FileDetailFragment) details).updateFileDetails(false, true); - } else { - onBackPressed(); - } - - // Force the preview if the file is an image or text file - if (uploadWasFine) { - OCFile ocFile = getFile(); - if (PreviewImageFragment.canBePreviewed(ocFile)) { - startImagePreview(getFile(), true); - } else if (PreviewTextFileFragment.canBePreviewed(ocFile)) { - startTextPreview(ocFile, true); - } - // TODO what about other kind of previews? - } - } - OCFileListFragment ocFileListFragment = getListOfFilesFragment(); - if (ocFileListFragment != null) { - ocFileListFragment.setLoading(false); - } - } - - // TODO refactor this receiver, and maybe DownloadFinishReceiver; this method is duplicated :S - private boolean isAscendant(String linkedToRemotePath) { - OCFile currentDir = getCurrentDir(); - return currentDir != null && currentDir.getRemotePath().startsWith(linkedToRemotePath); - } - } - - - /** - * Class waiting for broadcast events from the {@link FileDownloadWorker} service. - *

- * Updates the UI when a download is started or finished, provided that it is relevant for the current folder. - */ - private class DownloadFinishReceiver extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - boolean sameAccount = isSameAccount(intent); - String downloadedRemotePath = intent.getStringExtra(FileDownloadWorker.EXTRA_REMOTE_PATH); - String downloadBehaviour = intent.getStringExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR); - boolean isDescendant = isDescendant(downloadedRemotePath); - - if (sameAccount && isDescendant) { - String linkedToRemotePath = intent.getStringExtra(FileDownloadWorker.EXTRA_LINKED_TO_PATH); - if (linkedToRemotePath == null || isAscendant(linkedToRemotePath)) { - updateListOfFilesFragment(false); - } - refreshDetailsFragmentIfVisible(intent.getAction(), downloadedRemotePath, intent.getBooleanExtra(FileDownloadWorker.EXTRA_DOWNLOAD_RESULT, false)); - } - - if (mWaitingToSend != null) { - // update file after downloading - mWaitingToSend = getStorageManager().getFileByRemoteId(mWaitingToSend.getRemoteId()); - if (mWaitingToSend != null && mWaitingToSend.isDown() && OCFileListFragment.DOWNLOAD_SEND.equals(downloadBehaviour)) { - String packageName = intent.getStringExtra(SendShareDialog.PACKAGE_NAME); - String activityName = intent.getStringExtra(SendShareDialog.ACTIVITY_NAME); - - sendDownloadedFile(packageName, activityName); - } - } - - if (mWaitingToPreview != null) { - mWaitingToPreview = getStorageManager().getFileByRemoteId(mWaitingToPreview.getRemoteId()); - if (mWaitingToPreview != null && mWaitingToPreview.isDown() && EditImageActivity.OPEN_IMAGE_EDITOR.equals(downloadBehaviour)) { - startImageEditor(mWaitingToPreview); - } - } - } - - private boolean isDescendant(String downloadedRemotePath) { - OCFile currentDir = getCurrentDir(); - return currentDir != null && downloadedRemotePath != null && downloadedRemotePath.startsWith(currentDir.getRemotePath()); - } - - private boolean isAscendant(String linkedToRemotePath) { - OCFile currentDir = getCurrentDir(); - return currentDir != null && currentDir.getRemotePath().startsWith(linkedToRemotePath); - } - - private boolean isSameAccount(Intent intent) { - String accountName = intent.getStringExtra(FileDownloadWorker.EXTRA_ACCOUNT_NAME); - return accountName != null && getAccount() != null && accountName.equals(getAccount().name); - } - } - - - public void browseToRoot() { - OCFileListFragment listOfFiles = getListOfFilesFragment(); - if (listOfFiles != null) { // should never be null, indeed - OCFile root = getStorageManager().getFileByPath(OCFile.ROOT_PATH); - listOfFiles.listDirectory(root, MainApp.isOnlyOnDevice(), false); - setFile(listOfFiles.getCurrentFile()); - startSyncFolderOperation(root, false); - } - binding.fabMain.setImageResource(R.drawable.ic_plus); - resetTitleBarAndScrolling(); - } - - - @Override - public void onBrowsedDownTo(OCFile directory) { - setFile(directory); - resetTitleBarAndScrolling(); - // Sync Folder - startSyncFolderOperation(directory, false); - } - - /** - * Shows the information of the {@link OCFile} received as a parameter. - * - * @param file {@link OCFile} whose details will be shown - */ - @Override - public void showDetails(OCFile file) { - showDetails(file, 0); - } - - /** - * Shows the information of the {@link OCFile} received as a parameter. - * - * @param file {@link OCFile} whose details will be shown - * @param activeTab the active tab in the details view - */ - public void showDetails(OCFile file, int activeTab) { - User currentUser = getUser().orElseThrow(RuntimeException::new); - - resetScrolling(true); - - Fragment detailFragment = FileDetailFragment.newInstance(file, currentUser, activeTab); - setLeftFragment(detailFragment, false); - configureToolbarForPreview(file); - } - - /** - * Prevents content scrolling and toolbar collapse - */ - @VisibleForTesting - public void lockScrolling() { - binding.appbar.appbar.setExpanded(true, false); - final AppBarLayout.LayoutParams appbarParams = (AppBarLayout.LayoutParams) binding.appbar.toolbarFrame.getLayoutParams(); - appbarParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL); - binding.appbar.toolbarFrame.setLayoutParams(appbarParams); - } - - /** - * Resets content scrolling and toolbar collapse - */ - @VisibleForTesting - public void resetScrolling(boolean expandAppBar) { - AppBarLayout.LayoutParams appbarParams = (AppBarLayout.LayoutParams) binding.appbar.toolbarFrame.getLayoutParams(); - appbarParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS); - binding.appbar.toolbarFrame.setLayoutParams(appbarParams); - if (expandAppBar) { - binding.appbar.appbar.setExpanded(true, false); - } - } - - @Override - public void updateActionBarTitleAndHomeButton(OCFile chosenFile) { - if (chosenFile == null) { - chosenFile = getFile(); // if no file is passed, current file decides - } - super.updateActionBarTitleAndHomeButton(chosenFile); - } - - @Override - public boolean isDrawerIndicatorAvailable() { - return isRoot(getCurrentDir()); - } - - private void observeWorkerState() { - WorkerStateLiveData.Companion.instance().observe(this, state -> { - if (state instanceof WorkerState.Download) { - Log_OC.d(TAG, "Download worker started"); - handleDownloadWorkerState(); - } else if (state instanceof WorkerState.Idle) { - fileDownloadProgressListener = null; - } - }); - } - - private void handleDownloadWorkerState() { - if (mWaitingToPreview != null && getStorageManager() != null) { - mWaitingToPreview = getStorageManager().getFileById(mWaitingToPreview.getFileId()); - if (mWaitingToPreview != null && !mWaitingToPreview.isDown()) { - requestForDownload(); - } - } - } - - @Override - protected ServiceConnection newTransferenceServiceConnection() { - return new ListServiceConnection(); - } - - /** - * Defines callbacks for service binding, passed to bindService() - * TODO: Check if this can be removed since download and uploads uses work manager now. - */ - private class ListServiceConnection implements ServiceConnection { - - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - } - - @Override - public void onServiceDisconnected(ComponentName component) { - if (component.equals(new ComponentName(FileDisplayActivity.this, FileDownloadWorker.class))) { - Log_OC.d(TAG, "Download service disconnected"); - fileDownloadProgressListener = null; - } - } - } - - /** - * Updates the view associated to the activity after the finish of some operation over files in the current - * account. - * - * @param operation Removal operation performed. - * @param result Result of the removal. - */ - @Override - public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result) { - super.onRemoteOperationFinish(operation, result); - - if (operation instanceof RemoveFileOperation) { - onRemoveFileOperationFinish((RemoveFileOperation) operation, result); - } else if (operation instanceof RenameFileOperation) { - onRenameFileOperationFinish((RenameFileOperation) operation, result); - } else if (operation instanceof SynchronizeFileOperation) { - onSynchronizeFileOperationFinish((SynchronizeFileOperation) operation, result); - } else if (operation instanceof CreateFolderOperation) { - onCreateFolderOperationFinish((CreateFolderOperation) operation, result); - } else if (operation instanceof MoveFileOperation) { - onMoveFileOperationFinish((MoveFileOperation) operation, result); - } else if (operation instanceof CopyFileOperation) { - onCopyFileOperationFinish((CopyFileOperation) operation, result); - } else if (operation instanceof RestoreFileVersionRemoteOperation) { - onRestoreFileVersionOperationFinish(result); - } - } - - private void refreshShowDetails() { - Fragment details = getLeftFragment(); - if (details instanceof FileFragment) { - OCFile file = ((FileFragment) details).getFile(); - if (file != null) { - file = getStorageManager().getFileByPath(file.getRemotePath()); - if (details instanceof PreviewTextFragment) { - // Refresh OCFile of the fragment - ((PreviewTextFileFragment) details).updateFile(file); - } else { - showDetails(file); - } - } - supportInvalidateOptionsMenu(); - } - } - - /** - * Updates the view associated to the activity after the finish of an operation trying to remove a file. - * - * @param operation Removal operation performed. - * @param result Result of the removal. - */ - private void onRemoveFileOperationFinish(RemoveFileOperation operation, RemoteOperationResult result) { - - if (!operation.isInBackground()) { - DisplayUtils.showSnackMessage(this, ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources())); - } - - if (result.isSuccess()) { - OCFile removedFile = operation.getFile(); - tryStopPlaying(removedFile); - Fragment leftFragment = getLeftFragment(); - - // check if file is still available, if so do nothing - boolean fileAvailable = getStorageManager().fileExists(removedFile.getFileId()); - if (leftFragment instanceof FileFragment && !fileAvailable && removedFile.equals(((FileFragment) leftFragment).getFile())) { - setFile(getStorageManager().getFileById(removedFile.getParentId())); - resetTitleBarAndScrolling(); - } - OCFile parentFile = getStorageManager().getFileById(removedFile.getParentId()); - if (parentFile != null && parentFile.equals(getCurrentDir())) { - updateListOfFilesFragment(false); - } else if (getLeftFragment() instanceof GalleryFragment galleryFragment) { - galleryFragment.onRefresh(); - } - supportInvalidateOptionsMenu(); - } else { - if (result.isSslRecoverableException()) { - mLastSslUntrustedServerResult = result; - showUntrustedCertDialog(mLastSslUntrustedServerResult); - } - } - } - - private void onRestoreFileVersionOperationFinish(RemoteOperationResult result) { - if (result.isSuccess()) { - OCFile file = getFile(); - - // delete old local copy - if (file.isDown()) { - List list = new ArrayList<>(); - list.add(file); - getFileOperationsHelper().removeFiles(list, true, true); - - // download new version, only if file was previously download - getFileOperationsHelper().syncFile(file); - } - - OCFile parent = getStorageManager().getFileById(file.getParentId()); - startSyncFolderOperation(parent, true, true); - - Fragment leftFragment = getLeftFragment(); - if (leftFragment instanceof FileDetailFragment) { - FileDetailFragment fileDetailFragment = (FileDetailFragment) leftFragment; - fileDetailFragment.getFileDetailActivitiesFragment().reload(); - } - - DisplayUtils.showSnackMessage(this, R.string.file_version_restored_successfully); - } else { - DisplayUtils.showSnackMessage(this, R.string.file_version_restored_error); - } - } - - private void tryStopPlaying(OCFile file) { - // placeholder for stop-on-delete future code - if (mPlayerConnection != null && MimeTypeUtil.isAudio(file) && mPlayerConnection.isPlaying()) { - mPlayerConnection.stop(file); - } - } - - /** - * Updates the view associated to the activity after the finish of an operation trying to move a file. - * - * @param operation Move operation performed. - * @param result Result of the move operation. - */ - private void onMoveFileOperationFinish(MoveFileOperation operation, RemoteOperationResult result) { - if (result.isSuccess()) { - syncAndUpdateFolder(true); - } else { - try { - DisplayUtils.showSnackMessage(this, ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources())); - - } catch (NotFoundException e) { - Log_OC.e(TAG, "Error while trying to show fail message ", e); - } - } - } - - /** - * Updates the view associated to the activity after the finish of an operation trying to copy a file. - * - * @param operation Copy operation performed. - * @param result Result of the copy operation. - */ - private void onCopyFileOperationFinish(CopyFileOperation operation, RemoteOperationResult result) { - if (result.isSuccess()) { - updateListOfFilesFragment(false); - } else { - try { - DisplayUtils.showSnackMessage(this, ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources())); - - } catch (NotFoundException e) { - Log_OC.e(TAG, "Error while trying to show fail message ", e); - } - } - } - - /** - * Updates the view associated to the activity after the finish of an operation trying to rename a file. - * - * @param operation Renaming operation performed. - * @param result Result of the renaming. - */ - private void onRenameFileOperationFinish(RenameFileOperation operation, RemoteOperationResult result) { - Optional optionalUser = getUser(); - OCFile renamedFile = operation.getFile(); - if (result.isSuccess() && optionalUser.isPresent()) { - final User currentUser = optionalUser.get(); - Fragment leftFragment = getLeftFragment(); - if (leftFragment instanceof FileFragment) { - final FileFragment fileFragment = (FileFragment) leftFragment; - if (fileFragment instanceof FileDetailFragment && renamedFile.equals(fileFragment.getFile())) { - ((FileDetailFragment) fileFragment).updateFileDetails(renamedFile, currentUser); - showDetails(renamedFile); - - } else if (fileFragment instanceof PreviewMediaFragment && renamedFile.equals(fileFragment.getFile())) { - ((PreviewMediaFragment) fileFragment).updateFile(renamedFile); - if (PreviewMediaFragment.canBePreviewed(renamedFile)) { - long position = ((PreviewMediaFragment) fileFragment).getPosition(); - startMediaPreview(renamedFile, position, true, true, true, false); - } else { - getFileOperationsHelper().openFile(renamedFile); - } - } else if (fileFragment instanceof PreviewTextFragment && renamedFile.equals(fileFragment.getFile())) { - ((PreviewTextFileFragment) fileFragment).updateFile(renamedFile); - if (PreviewTextFileFragment.canBePreviewed(renamedFile)) { - startTextPreview(renamedFile, true); - } else { - getFileOperationsHelper().openFile(renamedFile); - } - } - } - - OCFile file = getStorageManager().getFileById(renamedFile.getParentId()); - if (file != null && file.equals(getCurrentDir())) { - updateListOfFilesFragment(false); - } - - } else { - DisplayUtils.showSnackMessage(this, ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources())); - - if (result.isSslRecoverableException()) { - mLastSslUntrustedServerResult = result; - showUntrustedCertDialog(mLastSslUntrustedServerResult); - } - } - } - - - private void onSynchronizeFileOperationFinish(SynchronizeFileOperation operation, RemoteOperationResult result) { - if (result.isSuccess() && operation.transferWasRequested()) { - OCFile syncedFile = operation.getLocalFile(); - onTransferStateChanged(syncedFile, true, true); - supportInvalidateOptionsMenu(); - refreshShowDetails(); - } - } - - /** - * Updates the view associated to the activity after the finish of an operation trying create a new folder - * - * @param operation Creation operation performed. - * @param result Result of the creation. - */ - private void onCreateFolderOperationFinish(CreateFolderOperation operation, RemoteOperationResult result) { - if (result.isSuccess()) { - OCFileListFragment fileListFragment = getListOfFilesFragment(); - if (fileListFragment != null) { - fileListFragment.onItemClicked(getStorageManager().getFileByDecryptedRemotePath(operation.getRemotePath())); - } - } else { - try { - if (ResultCode.FOLDER_ALREADY_EXISTS == result.getCode()) { - DisplayUtils.showSnackMessage(this, R.string.folder_already_exists); - } else { - DisplayUtils.showSnackMessage(this, ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources())); - } - } catch (NotFoundException e) { - Log_OC.e(TAG, "Error while trying to show fail message ", e); - } - } - } - - - /** - * {@inheritDoc} - */ - @Override - public void onTransferStateChanged(OCFile file, boolean downloading, boolean uploading) { - updateListOfFilesFragment(false); - Fragment leftFragment = getLeftFragment(); - Optional optionalUser = getUser(); - if (leftFragment instanceof FileDetailFragment && file.equals(((FileDetailFragment) leftFragment).getFile()) && optionalUser.isPresent()) { - final User currentUser = optionalUser.get(); - if (downloading || uploading) { - ((FileDetailFragment) leftFragment).updateFileDetails(file, currentUser); - } else { - if (!file.fileExists()) { - resetTitleBarAndScrolling(); - } else { - ((FileDetailFragment) leftFragment).updateFileDetails(false, true); - } - } - } - - } - - - private void requestForDownload() { - User user = getUser().orElseThrow(RuntimeException::new); - FileDownloadHelper.Companion.instance().downloadFileIfNotStartedBefore(user, mWaitingToPreview); - } - - @Override - public void onSavedCertificate() { - startSyncFolderOperation(getCurrentDir(), false); - } - - /** - * Starts an operation to refresh the requested folder. - *

- * The operation is run in a new background thread created on the fly. - *

- * The refresh updates is a "light sync": properties of regular files in folder are updated (including associated - * shares), but not their contents. Only the contents of files marked to be kept-in-sync are synchronized too. - * - * @param folder Folder to refresh. - * @param ignoreETag If 'true', the data from the server will be fetched and sync'ed even if the eTag didn't - * change. - */ - public void startSyncFolderOperation(OCFile folder, boolean ignoreETag) { - startSyncFolderOperation(folder, ignoreETag, false); - } - - /** - * Starts an operation to refresh the requested folder. - *

- * The operation is run in a new background thread created on the fly. - *

- * The refresh updates is a "light sync": properties of regular files in folder are updated (including associated - * shares), but not their contents. Only the contents of files marked to be kept-in-sync are synchronized too. - * - * @param folder Folder to refresh. - * @param ignoreETag If 'true', the data from the server will be fetched and sync'ed even if the eTag didn't - * change. - * @param ignoreFocus reloads file list even without focus, e.g. on tablet mode, focus can still be in detail view - */ - public void startSyncFolderOperation(final OCFile folder, final boolean ignoreETag, boolean ignoreFocus) { - - // the execution is slightly delayed to allow the activity get the window focus if it's being started - // or if the method is called from a dialog that is being dismissed - if (TextUtils.isEmpty(searchQuery) && getUser().isPresent()) { - getHandler().postDelayed(() -> { - Optional user = getUser(); - - if (!ignoreFocus && !hasWindowFocus() || !user.isPresent()) { - // do not refresh if the user rotates the device while another window has focus - // or if the current user is no longer valid - return; - } - - long currentSyncTime = System.currentTimeMillis(); - mSyncInProgress = true; - - // perform folder synchronization - RemoteOperation refreshFolderOperation = new RefreshFolderOperation(folder, currentSyncTime, false, ignoreETag, getStorageManager(), user.get(), getApplicationContext()); - refreshFolderOperation.execute(getAccount(), MainApp.getAppContext(), FileDisplayActivity.this, null, null); - - OCFileListFragment fragment = getListOfFilesFragment(); - - if (fragment != null && !(fragment instanceof GalleryFragment)) { - fragment.setLoading(true); - } - - setBackgroundText(); - }, DELAY_TO_REQUEST_REFRESH_OPERATION_LATER); - } - } - - private void requestForDownload(OCFile file, String downloadBehaviour, String packageName, String activityName) { - final User currentUser = getUser().orElseThrow(RuntimeException::new); - if (!FileDownloadHelper.Companion.instance().isDownloading(currentUser, file)) { - FileDownloadHelper.Companion.instance().downloadFile(currentUser, file, downloadBehaviour, DownloadType.DOWNLOAD, activityName, packageName, null); - } - } - - private void sendDownloadedFile(String packageName, String activityName) { - if (mWaitingToSend != null) { - - Intent sendIntent = IntentUtil.createSendIntent(this, mWaitingToSend); - sendIntent.setComponent(new ComponentName(packageName, activityName)); - - // Show dialog - String sendTitle = getString(R.string.activity_chooser_send_file_title); - startActivity(Intent.createChooser(sendIntent, sendTitle)); - } else { - Log_OC.e(TAG, "Trying to send a NULL OCFile"); - } - - mWaitingToSend = null; - } - - /** - * Requests the download of the received {@link OCFile} , updates the UI to monitor the download progress and - * prepares the activity to send the file when the download finishes. - * - * @param file {@link OCFile} to download and preview. - * @param packageName - * @param activityName - */ - public void startDownloadForSending(OCFile file, String downloadBehaviour, String packageName, String activityName) { - mWaitingToSend = file; - requestForDownload(mWaitingToSend, downloadBehaviour, packageName, activityName); - } - - public void startImagePreview(OCFile file, boolean showPreview) { - Intent showDetailsIntent = new Intent(this, PreviewImageActivity.class); - showDetailsIntent.putExtra(EXTRA_FILE, file); - showDetailsIntent.putExtra(EXTRA_LIVE_PHOTO_FILE, file.livePhotoVideo); - showDetailsIntent.putExtra(EXTRA_USER, getUser().orElseThrow(RuntimeException::new)); - if (showPreview) { - startActivity(showDetailsIntent); - } else { - FileOperationsHelper fileOperationsHelper = new FileOperationsHelper(this, getUserAccountManager(), connectivityService, editorUtils); - fileOperationsHelper.startSyncForFileAndIntent(file, showDetailsIntent); - } - } - - public void startImagePreview(OCFile file, VirtualFolderType type, boolean showPreview) { - Intent showDetailsIntent = new Intent(this, PreviewImageActivity.class); - showDetailsIntent.putExtra(PreviewImageActivity.EXTRA_FILE, file); - showDetailsIntent.putExtra(EXTRA_LIVE_PHOTO_FILE, file.livePhotoVideo); - showDetailsIntent.putExtra(EXTRA_USER, getUser().orElseThrow(RuntimeException::new)); - showDetailsIntent.putExtra(PreviewImageActivity.EXTRA_VIRTUAL_TYPE, type); - - if (showPreview) { - startActivity(showDetailsIntent); - } else { - FileOperationsHelper fileOperationsHelper = new FileOperationsHelper(this, - getUserAccountManager(), - connectivityService, - editorUtils); - fileOperationsHelper.startSyncForFileAndIntent(file, showDetailsIntent); - } - } - - /** - * Stars the preview of an already down media {@link OCFile}. - * - * @param file Media {@link OCFile} to preview. - * @param startPlaybackPosition Media position where the playback will be started, in milliseconds. - * @param autoplay When 'true', the playback will start without user interactions. - */ - public void startMediaPreview(OCFile file, long startPlaybackPosition, boolean autoplay, boolean showPreview, boolean streamMedia, boolean showInActivity) { - Optional user = getUser(); - if (!user.isPresent()) { - return; // not reachable under normal conditions - } - if (showPreview && file.isDown() && !file.isDownloading() || streamMedia) { - if (showInActivity) { - startMediaActivity(file, startPlaybackPosition, autoplay, user); - } else { - configureToolbarForPreview(file); - Fragment mediaFragment = PreviewMediaFragment.newInstance(file, user.get(), startPlaybackPosition, autoplay, false); - setLeftFragment(mediaFragment, false); - } - } else { - Intent previewIntent = new Intent(); - previewIntent.putExtra(EXTRA_FILE, file); - previewIntent.putExtra(PreviewMediaFragment.EXTRA_START_POSITION, startPlaybackPosition); - previewIntent.putExtra(PreviewMediaFragment.EXTRA_AUTOPLAY, autoplay); - FileOperationsHelper fileOperationsHelper = new FileOperationsHelper(this, getUserAccountManager(), connectivityService, editorUtils); - fileOperationsHelper.startSyncForFileAndIntent(file, previewIntent); - } - } - - private void startMediaActivity(OCFile file, long startPlaybackPosition, boolean autoplay, Optional user) { - Intent previewMediaIntent = new Intent(this, PreviewMediaActivity.class); - previewMediaIntent.putExtra(PreviewMediaActivity.EXTRA_FILE, file); - previewMediaIntent.putExtra(PreviewMediaActivity.EXTRA_USER, user.get()); - previewMediaIntent.putExtra(PreviewMediaActivity.EXTRA_START_POSITION, startPlaybackPosition); - previewMediaIntent.putExtra(PreviewMediaActivity.EXTRA_AUTOPLAY, autoplay); - startActivity(previewMediaIntent); - } - - public void configureToolbarForPreview(OCFile file) { - lockScrolling(); - super.updateActionBarTitleAndHomeButton(file); - } - - /** - * Starts the preview of a text file {@link OCFile}. - * - * @param file Text {@link OCFile} to preview. - */ - public void startTextPreview(OCFile file, boolean showPreview) { - Optional optUser = getUser(); - if (!optUser.isPresent()) { - // remnants of old unsafe system; do not crash, silently stop - return; - } - User user = optUser.get(); - if (showPreview) { - PreviewTextFileFragment fragment = PreviewTextFileFragment.create(user, file, searchOpen, searchQuery); - setLeftFragment(fragment, false); - configureToolbarForPreview(file); - } else { - Intent previewIntent = new Intent(); - previewIntent.putExtra(EXTRA_FILE, file); - previewIntent.putExtra(TEXT_PREVIEW, true); - FileOperationsHelper fileOperationsHelper = new FileOperationsHelper(this, getUserAccountManager(), connectivityService, editorUtils); - fileOperationsHelper.startSyncForFileAndIntent(file, previewIntent); - } - } - - /** - * Starts rich workspace preview for a folder. - * - * @param folder {@link OCFile} to preview its rich workspace. - */ - public void startRichWorkspacePreview(OCFile folder) { - Bundle args = new Bundle(); - args.putParcelable(EXTRA_FILE, folder); - configureToolbarForPreview(folder); - Fragment textPreviewFragment = Fragment.instantiate(getApplicationContext(), PreviewTextStringFragment.class.getName(), args); - setLeftFragment(textPreviewFragment, false); - } - - public void startContactListFragment(OCFile file) { - final User user = getUser().orElseThrow(RuntimeException::new); - ContactsPreferenceActivity.startActivityWithContactsFile(this, user, file); - } - - public void startPdfPreview(OCFile file) { - if (getFileOperationsHelper().canOpenFile(file)) { - // prefer third party PDF apps - getFileOperationsHelper().openFile(file); - } else { - final Fragment pdfFragment = PreviewPdfFragment.newInstance(file); - - setLeftFragment(pdfFragment, false); - configureToolbarForPreview(file); - setMainFabVisible(false); - } - } - - - /** - * Requests the download of the received {@link OCFile} , updates the UI to monitor the download progress and - * prepares the activity to preview or open the file when the download finishes. - * - * @param file {@link OCFile} to download and preview. - * @param parentFolder {@link OCFile} containing above file - */ - public void startDownloadForPreview(OCFile file, OCFile parentFolder) { - final User currentUser = getUser().orElseThrow(RuntimeException::new); - Fragment detailFragment = FileDetailFragment.newInstance(file, parentFolder, currentUser); - setLeftFragment(detailFragment, false); - configureToolbarForPreview(file); - mWaitingToPreview = file; - requestForDownload(); - setFile(file); - } - - - /** - * Opens EditImageActivity with given file loaded. If file is not available locally, it will be synced before - * opening the image editor. - * - * @param file {@link OCFile} (image) to be loaded into image editor - */ - public void startImageEditor(OCFile file) { - if (file.isDown()) { - Intent editImageIntent = new Intent(this, EditImageActivity.class); - editImageIntent.putExtra(EditImageActivity.EXTRA_FILE, file); - startActivity(editImageIntent); - } else { - mWaitingToPreview = file; - requestForDownload(file, EditImageActivity.OPEN_IMAGE_EDITOR, getPackageName(), this.getClass().getSimpleName()); - updateActionBarTitleAndHomeButton(file); - setFile(file); - } - } - - - /** - * Request stopping the upload/download operation in progress over the given {@link OCFile} file. - * - * @param file {@link OCFile} file which operation are wanted to be cancel - */ - public void cancelTransference(OCFile file) { - getFileOperationsHelper().cancelTransference(file); - if (mWaitingToPreview != null && mWaitingToPreview.getRemotePath().equals(file.getRemotePath())) { - mWaitingToPreview = null; - } - if (mWaitingToSend != null && mWaitingToSend.getRemotePath().equals(file.getRemotePath())) { - mWaitingToSend = null; - } - onTransferStateChanged(file, false, false); - } - - /** - * Request stopping all upload/download operations in progress over the given {@link OCFile} files. - * - * @param files collection of {@link OCFile} files which operations are wanted to be cancel - */ - public void cancelTransference(Collection files) { - for (OCFile file : files) { - cancelTransference(file); - } - } - - @Override - public void onRefresh(boolean ignoreETag) { - syncAndUpdateFolder(ignoreETag); - } - - @Override - public void onRefresh() { - syncAndUpdateFolder(true); - } - - private void syncAndUpdateFolder(boolean ignoreETag) { - syncAndUpdateFolder(ignoreETag, false); - } - - private void syncAndUpdateFolder(boolean ignoreETag, boolean ignoreFocus) { - OCFileListFragment listOfFiles = getListOfFilesFragment(); - if (listOfFiles != null && !listOfFiles.isSearchFragment()) { - OCFile folder = listOfFiles.getCurrentFile(); - if (folder != null) { - startSyncFolderOperation(folder, ignoreETag, ignoreFocus); - } - } - } - - @Override - public void showFiles(boolean onDeviceOnly, boolean personalFiles) { - super.showFiles(onDeviceOnly, personalFiles); - if (onDeviceOnly) { - updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_item_on_device)); - } - OCFileListFragment ocFileListFragment = getListOfFilesFragment(); - if (ocFileListFragment != null && !(ocFileListFragment instanceof GalleryFragment) && !(ocFileListFragment instanceof SharedListFragment)) { - ocFileListFragment.refreshDirectory(); - } else { - setLeftFragment(new OCFileListFragment()); - } - } - - @Subscribe(threadMode = ThreadMode.BACKGROUND) - public void onMessageEvent(final SearchEvent event) { - if (SearchRemoteOperation.SearchType.PHOTO_SEARCH == event.getSearchType()) { - Log_OC.d(this, "Switch to photo search fragment"); - setLeftFragment(new GalleryFragment()); - } else if (event.getSearchType() == SearchRemoteOperation.SearchType.SHARED_FILTER) { - Log_OC.d(this, "Switch to Shared fragment"); - setLeftFragment(new SharedListFragment()); - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onMessageEvent(SyncEventFinished event) { - Bundle bundle = event.getIntent().getExtras(); - if (event.getIntent().getBooleanExtra(TEXT_PREVIEW, false)) { - startTextPreview((OCFile) bundle.get(EXTRA_FILE), true); - } else if (bundle.containsKey(PreviewMediaFragment.EXTRA_START_POSITION)) { - startMediaPreview((OCFile) bundle.get(EXTRA_FILE), (long) bundle.get(PreviewMediaFragment.EXTRA_START_POSITION), (boolean) bundle.get(PreviewMediaFragment.EXTRA_AUTOPLAY), true, true, true); - } else if (bundle.containsKey(PreviewImageActivity.EXTRA_VIRTUAL_TYPE)) { - startImagePreview((OCFile) bundle.get(EXTRA_FILE), (VirtualFolderType) bundle.get(PreviewImageActivity.EXTRA_VIRTUAL_TYPE), true); - } else { - startImagePreview((OCFile) bundle.get(EXTRA_FILE), true); - } - } - - @Subscribe(threadMode = ThreadMode.BACKGROUND) - public void onMessageEvent(TokenPushEvent event) { - if (!preferences.isKeysReInitEnabled()) { - PushUtils.reinitKeys(getUserAccountManager()); - } else { - PushUtils.pushRegistrationToServer(getUserAccountManager(), preferences.getPushToken()); - } - } - - @Override - public void onStart() { - super.onStart(); - final Optional optionalUser = getUser(); - final FileDataStorageManager storageManager = getStorageManager(); - if (optionalUser.isPresent() && storageManager != null) { - /// Check whether the 'main' OCFile handled by the Activity is contained in the - // current Account - OCFile file = getFile(); - // get parent from path - String parentPath = ""; - if (file != null) { - if (file.isDown() && file.getLastSyncDateForProperties() == 0) { - // upload in progress - right now, files are not inserted in the local - // cache until the upload is successful get parent from path - parentPath = file.getRemotePath().substring(0, file.getRemotePath().lastIndexOf(file.getFileName())); - if (storageManager.getFileByPath(parentPath) == null) { - file = null; // not able to know the directory where the file is uploading - } - } else { - file = storageManager.getFileByPath(file.getRemotePath()); - // currentDir = null if not in the current Account - } - } - if (file == null) { - // fall back to root folder - file = storageManager.getFileByPath(OCFile.ROOT_PATH); // never returns null - } - setFile(file); - - User user = optionalUser.get(); - setupDrawer(); - - mSwitchAccountButton.setTag(user.getAccountName()); - DisplayUtils.setAvatar(user, this, getResources().getDimension(R.dimen.nav_drawer_menu_avatar_radius), getResources(), mSwitchAccountButton, this); - final boolean userChanged = !user.nameEquals(lastDisplayedUser.orElse(null)); - if (userChanged) { - Log_OC.d(TAG, "Initializing Fragments in onAccountChanged.."); - initFragments(); - if (file.isFolder() && TextUtils.isEmpty(searchQuery)) { - startSyncFolderOperation(file, false); - } - } else { - updateActionBarTitleAndHomeButton(file.isFolder() ? null : file); - } - } - lastDisplayedUser = optionalUser; - - EventBus.getDefault().post(new TokenPushEvent()); - checkForNewDevVersionNecessary(getApplicationContext()); - } - - private void registerRefreshFolderEventReceiver() { - IntentFilter filter = new IntentFilter(REFRESH_FOLDER_EVENT_RECEIVER); - LocalBroadcastManager.getInstance(this).registerReceiver(refreshFolderEventReceiver, filter); - } - - private final BroadcastReceiver refreshFolderEventReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - syncAndUpdateFolder(true); - } - }; - - @Override - protected void onDestroy() { - LocalBroadcastManager.getInstance(this).unregisterReceiver(refreshFolderEventReceiver); - super.onDestroy(); - } - - @Override - protected void onRestart() { - super.onRestart(); - - checkForNewDevVersionNecessary(getApplicationContext()); - } - - public void setSearchQuery(String query) { - searchQuery = query; - } - - private void handleOpenFileViaIntent(Intent intent) { - showLoadingDialog(getString(R.string.retrieving_file)); - - String userName = intent.getStringExtra(KEY_ACCOUNT); - String fileId = intent.getStringExtra(KEY_FILE_ID); - String filePath = intent.getStringExtra(KEY_FILE_PATH); - - if (userName == null && fileId == null && intent.getData() != null) { - openDeepLink(intent.getData()); - } else { - Optional optionalUser = userName == null ? getUser() : getUserAccountManager().getUser(userName); - if (optionalUser.isPresent()) { - if (!TextUtils.isEmpty(fileId)) { - openFile(optionalUser.get(), fileId); - } else if (!TextUtils.isEmpty(filePath)) { - openFileByPath(optionalUser.get(), filePath); - } else { - dismissLoadingDialog(); - accountClicked(optionalUser.get().hashCode()); - } - } else { - dismissLoadingDialog(); - DisplayUtils.showSnackMessage(this, getString(R.string.associated_account_not_found)); - } - } - } - - private void openDeepLink(Uri uri) { - DeepLinkHandler linkHandler = new DeepLinkHandler(getUserAccountManager()); - DeepLinkHandler.Match match = linkHandler.parseDeepLink(uri); - if (match == null) { - dismissLoadingDialog(); - DisplayUtils.showSnackMessage(this, getString(R.string.invalid_url)); - } else if (match.getUsers().isEmpty()) { - dismissLoadingDialog(); - DisplayUtils.showSnackMessage(this, getString(R.string.associated_account_not_found)); - } else if (match.getUsers().size() == SINGLE_USER_SIZE) { - openFile(match.getUsers().get(0), match.getFileId()); - } else { - selectUserAndOpenFile(match.getUsers(), match.getFileId()); - } - } - - private void selectUserAndOpenFile(List users, String fileId) { - final CharSequence[] userNames = new CharSequence[users.size()]; - for (int i = 0; i < userNames.length; i++) { - userNames[i] = users.get(i).getAccountName(); - } - final AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.common_choose_account).setItems(userNames, (dialog, which) -> { - User user = users.get(which); - openFile(user, fileId); - showLoadingDialog(getString(R.string.retrieving_file)); - }); - final AlertDialog dialog = builder.create(); - dismissLoadingDialog(); - dialog.show(); - } - - private void openFile(User user, String fileId) { - setUser(user); - - if (fileId == null) { - onFileRequestError(null); - return; - } - - FileDataStorageManager storageManager = getStorageManager(); - - if (storageManager == null) { - storageManager = new FileDataStorageManager(user, getContentResolver()); - } - - FetchRemoteFileTask fetchRemoteFileTask = new FetchRemoteFileTask(user, fileId, storageManager, this); - fetchRemoteFileTask.execute(); - } - - private void openFileByPath(User user, String filepath) { - setUser(user); - - if (filepath == null) { - onFileRequestError(null); - return; - } - - FileDataStorageManager storageManager = getStorageManager(); - - if (storageManager == null) { - storageManager = new FileDataStorageManager(user, getContentResolver()); - } - - OwnCloudClient client; - try { - client = clientFactory.create(user); - } catch (ClientFactory.CreationException e) { - onFileRequestError(null); - return; - } - - GetRemoteFileTask getRemoteFileTask = new GetRemoteFileTask(this, filepath, client, storageManager, user); - asyncRunner.postQuickTask(getRemoteFileTask, this::onFileRequestResult, this::onFileRequestError); - } - - private Unit onFileRequestError(Throwable throwable) { - dismissLoadingDialog(); - DisplayUtils.showSnackMessage(this, getString(R.string.error_retrieving_file)); - Log_OC.e(TAG, "Requesting file from remote failed!", throwable); - return null; - } - - - private Unit onFileRequestResult(GetRemoteFileTask.Result result) { - dismissLoadingDialog(); - - setFile(result.getFile()); - - OCFileListFragment fileFragment = new OCFileListFragment(); - setLeftFragment(fileFragment); - - getSupportFragmentManager().executePendingTransactions(); - - fileFragment.onItemClicked(result.getFile()); - - return null; - } - - public void performUnifiedSearch(String query, ArrayList listOfHiddenFiles) { - UnifiedSearchFragment unifiedSearchFragment = UnifiedSearchFragment.Companion.newInstance(query, listOfHiddenFiles); - setLeftFragment(unifiedSearchFragment, false); - } - - public void setMainFabVisible(final boolean visible) { - final int visibility = visible ? View.VISIBLE : View.GONE; - binding.fabMain.setVisibility(visibility); - } - - public void showFile(OCFile selectedFile, String message) { - dismissLoadingDialog(); - - OCFileListFragment listOfFiles = getOCFileListFragmentFromFile(); - - if (TextUtils.isEmpty(message)) { - OCFile temp = getFile(); - setFile(getCurrentDir()); - listOfFiles.listDirectory(getCurrentDir(), temp, MainApp.isOnlyOnDevice(), false); - updateActionBarTitleAndHomeButton(null); - } else { - DisplayUtils.showSnackMessage(listOfFiles.getView(), message); - } - - if (selectedFile != null) { - listOfFiles.onItemClicked(selectedFile); - } - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt new file mode 100644 index 0000000..82dc1ea --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -0,0 +1,3089 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-FileCopyrightText: 2023-2024 TSI-mc + * SPDX-FileCopyrightText: 2023 Archontis E. Kostis + * SPDX-FileCopyrightText: 2019 Chris Narkiewicz + * SPDX-FileCopyrightText: 2018-2022 Tobias Kaminsky + * SPDX-FileCopyrightText: 2018-2020 Andy Scherzinger + * SPDX-FileCopyrightText: 2016 ownCloud Inc. + * SPDX-FileCopyrightText: 2012-2013 David A. Velasco + * SPDX-FileCopyrightText: 2011 Bartosz Przybylski + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) + */ +package com.owncloud.android.ui.activity + +import android.accounts.AuthenticatorException +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.content.IntentFilter +import android.content.ServiceConnection +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.content.res.Resources +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.os.Parcelable +import android.text.TextUtils +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewTreeObserver.OnGlobalLayoutListener +import android.view.WindowManager.BadTokenException +import androidx.activity.OnBackPressedCallback +import androidx.annotation.VisibleForTesting +import androidx.appcompat.widget.SearchView +import androidx.core.view.MenuItemCompat +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.appReview.InAppReviewHelper +import com.nextcloud.client.account.User +import com.nextcloud.client.appinfo.AppInfo +import com.nextcloud.client.core.AsyncRunner +import com.nextcloud.client.core.Clock +import com.nextcloud.client.di.Injectable +import com.nextcloud.client.editimage.EditImageActivity +import com.nextcloud.client.files.DeepLinkHandler +import com.nextcloud.client.jobs.download.FileDownloadHelper +import com.nextcloud.client.jobs.download.FileDownloadWorker +import com.nextcloud.client.jobs.download.FileDownloadWorker.Companion.getDownloadAddedMessage +import com.nextcloud.client.jobs.download.FileDownloadWorker.Companion.getDownloadFinishMessage +import com.nextcloud.client.jobs.upload.FileUploadHelper +import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.nextcloud.client.jobs.upload.FileUploadWorker.Companion.getUploadFinishMessage +import com.nextcloud.client.media.PlayerServiceConnection +import com.nextcloud.client.network.ClientFactory.CreationException +import com.nextcloud.client.preferences.AppPreferences +import com.nextcloud.client.utils.IntentUtil +import com.nextcloud.model.ToolbarItem +import com.nextcloud.model.ToolbarStyle +import com.nextcloud.model.WorkerState +import com.nextcloud.model.WorkerState.DownloadFinished +import com.nextcloud.model.WorkerState.DownloadStarted +import com.nextcloud.model.WorkerState.OfflineOperationsCompleted +import com.nextcloud.model.WorkerState.UploadFinished +import com.nextcloud.model.WorkerStateLiveData +import com.nextcloud.utils.extensions.getParcelableArgument +import com.nextcloud.utils.extensions.isActive +import com.nextcloud.utils.extensions.lastFragment +import com.nextcloud.utils.extensions.logFileSize +import com.nextcloud.utils.extensions.navigateToAllFiles +import com.nextcloud.utils.fileNameValidator.FileNameValidator.checkFolderPath +import com.nextcloud.utils.view.FastScrollUtils +import com.owncloud.android.MainApp +import com.owncloud.android.R +import com.owncloud.android.databinding.FilesBinding +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.SyncedFolderProvider +import com.owncloud.android.datamodel.VirtualFolderType +import com.owncloud.android.files.services.NameCollisionPolicy +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.files.RestoreFileVersionRemoteOperation +import com.owncloud.android.lib.resources.files.SearchRemoteOperation +import com.owncloud.android.lib.resources.notifications.GetNotificationsRemoteOperation +import com.owncloud.android.operations.CopyFileOperation +import com.owncloud.android.operations.CreateFolderOperation +import com.owncloud.android.operations.DownloadType +import com.owncloud.android.operations.MoveFileOperation +import com.owncloud.android.operations.RefreshFolderOperation +import com.owncloud.android.operations.RemoveFileOperation +import com.owncloud.android.operations.RenameFileOperation +import com.owncloud.android.operations.SynchronizeFileOperation +import com.owncloud.android.operations.UploadFileOperation +import com.owncloud.android.syncadapter.FileSyncAdapter +import com.owncloud.android.ui.CompletionCallback +import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask +import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask.CheckAvailableSpaceListener +import com.owncloud.android.ui.asynctasks.FetchRemoteFileTask +import com.owncloud.android.ui.asynctasks.GetRemoteFileTask +import com.owncloud.android.ui.dialog.SendShareDialog +import com.owncloud.android.ui.dialog.SendShareDialog.SendShareDialogDownloader +import com.owncloud.android.ui.dialog.SortingOrderDialogFragment.OnSortingOrderListener +import com.owncloud.android.ui.dialog.StoragePermissionDialogFragment +import com.owncloud.android.ui.dialog.TermsOfServiceDialog +import com.owncloud.android.ui.events.SearchEvent +import com.owncloud.android.ui.events.SyncEventFinished +import com.owncloud.android.ui.events.TokenPushEvent +import com.owncloud.android.ui.fragment.EmptyListState +import com.owncloud.android.ui.fragment.FileDetailFragment +import com.owncloud.android.ui.fragment.FileFragment +import com.owncloud.android.ui.fragment.GalleryFragment +import com.owncloud.android.ui.fragment.GroupfolderListFragment +import com.owncloud.android.ui.fragment.OCFileListFragment +import com.owncloud.android.ui.fragment.SearchType +import com.owncloud.android.ui.fragment.SharedListFragment +import com.owncloud.android.ui.fragment.TaskRetainerFragment +import com.owncloud.android.ui.fragment.UnifiedSearchFragment +import com.owncloud.android.ui.helpers.FileOperationsHelper +import com.owncloud.android.ui.helpers.UriUploader +import com.owncloud.android.ui.interfaces.TransactionInterface +import com.owncloud.android.ui.preview.PreviewImageActivity +import com.owncloud.android.ui.preview.PreviewImageFragment +import com.owncloud.android.ui.preview.PreviewMediaActivity +import com.owncloud.android.ui.preview.PreviewMediaFragment +import com.owncloud.android.ui.preview.PreviewMediaFragment.Companion.newInstance +import com.owncloud.android.ui.preview.PreviewTextFileFragment +import com.owncloud.android.ui.preview.PreviewTextFragment +import com.owncloud.android.ui.preview.PreviewTextStringFragment +import com.owncloud.android.ui.preview.pdf.PreviewPdfFragment.Companion.newInstance +import com.owncloud.android.utils.DataHolderUtil +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.ErrorMessageAdapter +import com.owncloud.android.utils.FileSortOrder +import com.owncloud.android.utils.MimeTypeUtil +import com.owncloud.android.utils.PermissionUtil +import com.owncloud.android.utils.PermissionUtil.requestExternalStoragePermission +import com.owncloud.android.utils.PermissionUtil.requestNotificationPermission +import com.owncloud.android.utils.PushUtils +import com.owncloud.android.utils.StringUtils +import com.owncloud.android.utils.theme.CapabilityUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import java.io.File +import java.util.function.Supplier +import javax.inject.Inject + +/** + * Displays, what files the user has available in his Nextcloud. This is the main view. + */ +@Suppress( + "ComplexCondition", + "SpreadOperator", + "ForbiddenComment", + "ReturnCount", + "LargeClass", + "NestedBlockDepth", + "TooManyFunctions" +) +class FileDisplayActivity : + FileActivity(), + FileFragment.ContainerActivity, + OnEnforceableRefreshListener, + OnSortingOrderListener, + SendShareDialogDownloader, + Injectable { + private lateinit var binding: FilesBinding + + private var mSyncBroadcastReceiver: SyncBroadcastReceiver? = null + private var mUploadFinishReceiver: UploadFinishReceiver? = null + private var mDownloadFinishReceiver: DownloadFinishReceiver? = null + private var mLastSslUntrustedServerResult: RemoteOperationResult<*>? = null + + private var mWaitingToPreview: OCFile? = null + + private var mSyncInProgress: Boolean = false + set(value) { + field = value + setBackgroundText() + } + + private var mWaitingToSend: OCFile? = null + + private var mDrawerMenuItemstoShowHideList: MutableCollection? = null + + private var searchQuery: String? = "" + private var searchOpen = false + + private var searchView: SearchView? = null + private var mPlayerConnection: PlayerServiceConnection? = null + private var lastDisplayedAccountName: String? = null + + @Inject + lateinit var localBroadcastManager: LocalBroadcastManager + + @Inject + lateinit var preferences: AppPreferences + + @Inject + lateinit var appInfo: AppInfo + + @Inject + lateinit var inAppReviewHelper: InAppReviewHelper + + @Inject + lateinit var fastScrollUtils: FastScrollUtils + + @Inject + lateinit var asyncRunner: AsyncRunner + + @Inject + lateinit var clock: Clock + + @Inject + lateinit var syncedFolderProvider: SyncedFolderProvider + + /** + * Indicates whether the downloaded file should be previewed immediately. Since `FileDownloadWorker` can be + * triggered from multiple sources, this helps determine if an automatic preview is needed after download. + */ + private var fileIDForImmediatePreview: Long = -1 + + fun setFileIDForImmediatePreview(fileIDForImmediatePreview: Long) { + this.fileIDForImmediatePreview = fileIDForImmediatePreview + } + + @SuppressLint("UnsafeIntentLaunch") + override fun onCreate(savedInstanceState: Bundle?) { + Log_OC.v(TAG, "onCreate() start") + // Set the default theme to replace the launch screen theme. + setTheme(R.style.Theme_ownCloud_Toolbar_Drawer) + + super.onCreate(savedInstanceState) + lastDisplayedAccountName = preferences.lastDisplayedAccountName + + intent?.let { + handleCommonIntents(it) + handleAccountSwitchIntent(it) + } + + loadSavedInstanceState(savedInstanceState) + + /** USER INTERFACE */ + initLayout() + initUI() + initTaskRetainerFragment() + + // Restoring after UI has been inflated. + if (savedInstanceState != null) { + showSortListGroup(savedInstanceState.getBoolean(KEY_IS_SORT_GROUP_VISIBLE)) + } + + mPlayerConnection = PlayerServiceConnection(this) + + checkStoragePath() + + initSyncBroadcastReceiver() + observeWorkerState() + startMetadataSyncForRoot() + handleBackPress() + } + + private fun loadSavedInstanceState(savedInstanceState: Bundle?) { + if (savedInstanceState != null) { + mWaitingToPreview = + savedInstanceState.getParcelableArgument(KEY_WAITING_TO_PREVIEW, OCFile::class.java) + mSyncInProgress = savedInstanceState.getBoolean(KEY_SYNC_IN_PROGRESS) + mWaitingToSend = savedInstanceState.getParcelableArgument(KEY_WAITING_TO_SEND, OCFile::class.java) + searchQuery = savedInstanceState.getString(KEY_SEARCH_QUERY) + searchOpen = savedInstanceState.getBoolean(KEY_IS_SEARCH_OPEN, false) + } else { + mWaitingToPreview = null + mSyncInProgress = false + mWaitingToSend = null + } + } + + private fun initLayout() { + // Inflate and set the layout view + binding = FilesBinding.inflate(layoutInflater) + setContentView(binding.getRoot()) + } + + private fun initUI() { + setupHomeSearchToolbarWithSortAndListButtons() + mMenuButton.setOnClickListener { v: View? -> openDrawer() } + mSwitchAccountButton.setOnClickListener { v: View? -> showManageAccountsDialog() } + mNotificationButton.setOnClickListener { v: View? -> startActivity(NotificationsActivity::class.java) } + fastScrollUtils.fixAppBarForFastScroll(binding.appbar.appbar, binding.rootLayout) + } + + private fun initTaskRetainerFragment() { + // Init Fragment without UI to retain AsyncTask across configuration changes + val fm = supportFragmentManager + var taskRetainerFragment = + fm.findFragmentByTag(TaskRetainerFragment.FTAG_TASK_RETAINER_FRAGMENT) as TaskRetainerFragment? + if (taskRetainerFragment == null) { + taskRetainerFragment = TaskRetainerFragment() + fm.beginTransaction().add(taskRetainerFragment, TaskRetainerFragment.FTAG_TASK_RETAINER_FRAGMENT).commit() + } // else, Fragment already created and retained across configuration change + } + + private fun checkStoragePath() { + val newStorage = Environment.getExternalStorageDirectory().absolutePath + val storagePath = preferences.getStoragePath(newStorage) + if (!preferences.isStoragePathValid() && !File(storagePath).exists()) { + // falling back to default + preferences.setStoragePath(newStorage) + preferences.setStoragePathValid() + MainApp.setStoragePath(newStorage) + + try { + val builder = MaterialAlertDialogBuilder(this, R.style.Theme_ownCloud_Dialog) + .setTitle(R.string.wrong_storage_path) + .setMessage(R.string.wrong_storage_path_desc) + .setPositiveButton( + R.string.dialog_close + ) { dialog: DialogInterface?, which: Int -> dialog?.dismiss() } + .setIcon(R.drawable.ic_settings) + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(applicationContext, builder) + + builder.create().show() + } catch (e: BadTokenException) { + Log_OC.e(TAG, "Error showing wrong storage info, so skipping it: " + e.message) + } + } + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val fragment = + supportFragmentManager.findFragmentByTag( + PermissionUtil.PERMISSION_CHOICE_DIALOG_TAG + ) as StoragePermissionDialogFragment? + if (fragment != null) { + val dialog = fragment.dialog + + if (dialog != null && dialog.isShowing) { + dialog.dismiss() + supportFragmentManager.beginTransaction().remove(fragment).commitNowAllowingStateLoss() + requestExternalStoragePermission(this, viewThemeUtils) + } + } + } + } + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + + // handle notification permission on API level >= 33 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // request notification permission first and then prompt for storage permissions + // storage permissions handled in onRequestPermissionsResult + requestNotificationPermission(this) + } else { + requestExternalStoragePermission(this, viewThemeUtils) + } + + if (intent.getParcelableArgument( + OCFileListFragment.SEARCH_EVENT, + SearchEvent::class.java + ) != null + ) { + switchToSearchFragment(savedInstanceState) + setupDrawer() + } else { + createMinFragments(savedInstanceState) + } + + upgradeNotificationForInstantUpload() + checkOutdatedServer() + checkNotifications() + } + + /** + * For Android 7+. Opens a pop up info for the new instant upload and disabled the old instant upload. + */ + private fun upgradeNotificationForInstantUpload() { + // check for Android 6+ if legacy instant upload is activated --> disable + show info + if (preferences.instantPictureUploadEnabled() || preferences.instantVideoUploadEnabled()) { + preferences.removeLegacyPreferences() + // show info pop-up + MaterialAlertDialogBuilder(this, R.style.Theme_ownCloud_Dialog).setTitle(R.string.drawer_synced_folders) + .setMessage( + R.string.synced_folders_new_info + ).setPositiveButton( + R.string.drawer_open + ) { dialog: DialogInterface?, which: Int -> + // show instant upload + val syncedFoldersIntent = Intent(applicationContext, SyncedFoldersActivity::class.java) + dialog?.dismiss() + startActivity(syncedFoldersIntent) + }.setNegativeButton( + R.string.drawer_close + ) { dialog: DialogInterface?, which: Int -> dialog?.dismiss() } + .setIcon( + R.drawable.nav_synced_folders + ).show() + } + } + + private fun checkOutdatedServer() { + val user = getUser() + // show outdated warning + if (user.isPresent && + CapabilityUtils.checkOutdatedWarning( + getResources(), + user.get().server.version, + capabilities.extendedSupport.isTrue, + capabilities.hasValidSubscription.isTrue + ) + ) { + DisplayUtils.showServerOutdatedSnackbar(this, Snackbar.LENGTH_LONG) + } + } + + private fun checkNotifications() { + lifecycleScope.launch(Dispatchers.IO) { + try { + val result = GetNotificationsRemoteOperation() + .execute(clientFactory.createNextcloudClient(accountManager.user)) + + if (result.isSuccess && result.getResultData()?.isEmpty() == false) { + runOnUiThread { mNotificationButton.visibility = View.VISIBLE } + } else { + runOnUiThread { mNotificationButton.visibility = View.GONE } + } + } catch (_: CreationException) { + Log_OC.e(TAG, "Could not fetch notifications!") + } + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + when (requestCode) { + // handle notification permission on API level >= 33 + PermissionUtil.PERMISSIONS_POST_NOTIFICATIONS -> + // dialogue was dismissed -> prompt for storage permissions + requestExternalStoragePermission(this, viewThemeUtils) + + // If request is cancelled, result arrays are empty. + PermissionUtil.PERMISSIONS_EXTERNAL_STORAGE -> + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // permission was granted + EventBus.getDefault().post(TokenPushEvent()) + // toggle on is save since this is the only scenario this code gets accessed + } + + // If request is cancelled, result arrays are empty. + PermissionUtil.PERMISSIONS_CAMERA -> + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // permission was granted + getOCFileListFragmentFromFile(object : TransactionInterface { + override fun onOCFileListFragmentComplete(fragment: OCFileListFragment) { + fragment.directCameraUpload() + } + }) + } + + else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + } + + private fun switchToSearchFragment(savedInstanceState: Bundle?) { + if (savedInstanceState == null) { + val listOfFiles = OCFileListFragment() + val args = Bundle() + + args.putParcelable( + OCFileListFragment.SEARCH_EVENT, + intent + .getParcelableArgument( + OCFileListFragment.SEARCH_EVENT, + SearchEvent::class.java + ) + ) + args.putBoolean(OCFileListFragment.ARG_ALLOW_CONTEXTUAL_ACTIONS, true) + + listOfFiles.setArguments(args) + val transaction = supportFragmentManager.beginTransaction() + transaction.add(R.id.left_fragment_container, listOfFiles, TAG_LIST_OF_FILES) + transaction.commit() + } else { + supportFragmentManager.findFragmentByTag(TAG_LIST_OF_FILES) + } + } + + private fun createMinFragments(savedInstanceState: Bundle?) { + if (savedInstanceState == null) { + val listOfFiles = OCFileListFragment() + val args = Bundle() + args.putBoolean(OCFileListFragment.ARG_ALLOW_CONTEXTUAL_ACTIONS, true) + listOfFiles.setArguments(args) + val transaction = supportFragmentManager.beginTransaction() + transaction.add(R.id.left_fragment_container, listOfFiles, TAG_LIST_OF_FILES) + transaction.commit() + } else { + supportFragmentManager.findFragmentByTag(TAG_LIST_OF_FILES) + } + } + + private fun initFragments() { + /** First fragment */ + val listOfFiles = this.listOfFilesFragment + if (listOfFiles != null && TextUtils.isEmpty(searchQuery)) { + listOfFiles.listDirectory(getCurrentDir(), file, MainApp.isOnlyOnDevice(), false) + } else { + Log_OC.e(TAG, "Still have a chance to lose the initialization of list fragment >(") + } + + /** reset views */ + resetTitleBarAndScrolling() + } + + // region Handle Intents + @SuppressLint("UnsafeIntentLaunch") + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + handleCommonIntents(intent) + handleSpecialIntents(intent) + handleRestartIntent(intent) + } + + private fun handleSpecialIntents(intent: Intent) { + val action = intent.action + + when { + ACTION_DETAILS.equals(action, ignoreCase = true) -> { + val file = getFileFromIntent(intent) + setFile(file) + showDetails(file) + } + + Intent.ACTION_SEARCH == action -> handleSearchIntent(intent) + + ALL_FILES == action -> { + Log_OC.d(this, "Switch to oc file fragment") + menuItemId = R.id.nav_all_files + leftFragment = OCFileListFragment() + supportFragmentManager.executePendingTransactions() + browseToRoot() + } + + LIST_GROUPFOLDERS == action -> { + Log_OC.d(this, "Switch to list groupfolders fragment") + menuItemId = R.id.nav_groupfolders + leftFragment = GroupfolderListFragment() + supportFragmentManager.executePendingTransactions() + } + } + } + + @SuppressLint("UnsafeIntentLaunch") + private fun handleCommonIntents(intent: Intent) { + when (intent.action) { + Intent.ACTION_VIEW -> handleOpenFileViaIntent(intent) + OPEN_FILE -> { + onOpenFileIntent(intent) + } + } + } + + @SuppressLint("UnsafeIntentLaunch") + private fun handleRestartIntent(intent: Intent) { + if (intent.action != RESTART) { + return + } + + finish() + startActivity(intent) + } + + private fun handleAccountSwitchIntent(intent: Intent) { + if (intent.action != RESTART) { + return + } + + val accountName = accountManager.user.accountName + val message = getString(R.string.logged_in_as) + val snackBarMessage = String.format(message, accountName) + DisplayUtils.showSnackMessage(this, snackBarMessage) + } + + private fun handleSearchIntent(intent: Intent) { + val searchEvent = intent.getParcelableArgument( + OCFileListFragment.SEARCH_EVENT, + SearchEvent::class.java + ) ?: return + + when (searchEvent.searchType) { + SearchRemoteOperation.SearchType.PHOTO_SEARCH -> { + Log_OC.d(this, "Switch to photo search fragment") + val bundle = Bundle().apply { + putParcelable(OCFileListFragment.SEARCH_EVENT, searchEvent) + } + leftFragment = GalleryFragment().apply { + arguments = bundle + } + } + + SearchRemoteOperation.SearchType.SHARED_FILTER -> { + Log_OC.d(this, "Switch to shared fragment") + val bundle = Bundle().apply { + putParcelable(OCFileListFragment.SEARCH_EVENT, searchEvent) + } + leftFragment = SharedListFragment().apply { + arguments = bundle + } + } + + else -> { + Log_OC.d(this, "Switch to oc file search fragment") + val bundle = Bundle().apply { + putParcelable(OCFileListFragment.SEARCH_EVENT, searchEvent) + } + leftFragment = OCFileListFragment().apply { + arguments = bundle + } + } + } + } + // endregion + + private fun onOpenFileIntent(intent: Intent) { + val file = getFileFromIntent(intent) + if (file == null) { + Log_OC.e(TAG, "Can't open file intent, file is null") + return + } + + val currentFragment = leftFragment + + if (currentFragment == null) { + Log_OC.e(TAG, "Can't open file intent, left fragment is null") + return + } + + val fileListFragment: OCFileListFragment = when { + currentFragment is OCFileListFragment && currentFragment !is GalleryFragment -> { + currentFragment + } + + else -> { + Log_OC.w( + TAG, + "Left fragment is not a valid OCFileListFragment " + + "(was ${currentFragment::class.simpleName}). " + + "Replacing with OCFileListFragment." + ) + val newFragment = OCFileListFragment() + setLeftFragment(newFragment, false) + setupHomeSearchToolbarWithSortAndListButtons() + newFragment + } + } + + // Post to main thread to ensure fragment is fully attached before interacting + Handler(Looper.getMainLooper()).post { + fileListFragment.onItemClicked(file) + } + } + + private fun setLeftFragment(fragment: Fragment?, showSortListGroup: Boolean) { + if (fragment == null) { + return + } + + prepareFragmentBeforeCommit(showSortListGroup) + commitFragment( + fragment, + object : CompletionCallback { + override fun onComplete(isFragmentCommitted: Boolean) { + Log_OC.d( + TAG, + "Left fragment committed: $isFragmentCommitted" + ) + } + } + ) + } + + private fun prepareFragmentBeforeCommit(showSortListGroup: Boolean) { + searchView?.post { searchView?.setQuery(searchQuery, true) } + setDrawerIndicatorEnabled(false) + + // clear the subtitle while navigating to any other screen from Media screen + clearToolbarSubtitle() + + showSortListGroup(showSortListGroup) + } + + private fun commitFragment(fragment: Fragment, callback: CompletionCallback) { + val fragmentManager = supportFragmentManager + if (this.isActive() && !fragmentManager.isDestroyed) { + val transaction = fragmentManager.beginTransaction() + transaction.addToBackStack(null) + transaction.replace(R.id.left_fragment_container, fragment, TAG_LIST_OF_FILES) + transaction.commit() + callback.onComplete(true) + } else { + callback.onComplete(false) + } + } + + private fun getOCFileListFragmentFromFile(transaction: TransactionInterface) { + val leftFragment = this.leftFragment + + if (leftFragment is OCFileListFragment) { + transaction.onOCFileListFragmentComplete(leftFragment) + return + } + + val listOfFiles = OCFileListFragment() + val args = Bundle() + args.putBoolean(OCFileListFragment.ARG_ALLOW_CONTEXTUAL_ACTIONS, true) + listOfFiles.setArguments(args) + + runOnUiThread { + val fm = supportFragmentManager + if (!fm.isStateSaved && !fm.isDestroyed) { + prepareFragmentBeforeCommit(true) + commitFragment( + listOfFiles, + object : CompletionCallback { + override fun onComplete(value: Boolean) { + if (value) { + Log_OC.d(TAG, "OCFileListFragment committed, executing pending transaction") + fm.executePendingTransactions() + transaction.onOCFileListFragmentComplete(listOfFiles) + } else { + Log_OC.d( + TAG, + "OCFileListFragment not committed, skipping executing " + + "pending transaction" + ) + } + } + } + ) + } + } + } + + fun showFileActions(file: OCFile?) { + dismissLoadingDialog() + getOCFileListFragmentFromFile(object : TransactionInterface { + override fun onOCFileListFragmentComplete(fragment: OCFileListFragment) { + browseUp(fragment) + fragment.onOverflowIconClicked(file, null) + } + }) + } + + var leftFragment: Fragment? + get() = supportFragmentManager.findFragmentByTag(TAG_LIST_OF_FILES) + + /** + * Replaces the first fragment managed by the activity with the received as a parameter. + * + * @param fragment New Fragment to set. + */ + private set(fragment) { + setLeftFragment(fragment, true) + } + + @get:Deprecated("") + val listOfFilesFragment: OCFileListFragment? + get() { + val listOfFiles = + supportFragmentManager.findFragmentByTag(TAG_LIST_OF_FILES) + if (listOfFiles is OCFileListFragment) { + return listOfFiles + } + Log_OC.e(TAG, "Access to unexisting list of files fragment") + return null + } + + protected fun resetTitleBarAndScrolling() { + updateActionBarTitleAndHomeButton(null) + resetScrolling(true) + } + + fun updateListOfFilesFragment(fromSearch: Boolean) { + val fileListFragment = this.listOfFilesFragment + fileListFragment?.listDirectory(MainApp.isOnlyOnDevice(), fromSearch) + } + + fun resetSearchView() { + val fileListFragment = this.listOfFilesFragment + fileListFragment?.isSearchFragment = false + } + + protected fun refreshDetailsFragmentIfVisible( + downloadEvent: String, + downloadedRemotePath: String, + success: Boolean + ) { + val leftFragment = this.leftFragment + if (leftFragment is FileDetailFragment) { + val waitedPreview = mWaitingToPreview != null && mWaitingToPreview?.remotePath == downloadedRemotePath + val fileInFragment = leftFragment.file + if (fileInFragment != null && downloadedRemotePath != fileInFragment.remotePath) { + // the user browsed to other file ; forget the automatic preview + mWaitingToPreview = null + } else if (downloadEvent == getDownloadAddedMessage()) { + // grant that the details fragment updates the progress bar + leftFragment.listenForTransferProgress() + leftFragment.updateFileDetails(true, false) + } else if (downloadEvent == getDownloadFinishMessage()) { + // update the details panel + var detailsFragmentChanged = false + if (waitedPreview) { + if (success) { + // update the file from database, for the local storage path + mWaitingToPreview = mWaitingToPreview?.fileId?.let { storageManager.getFileById(it) } + + if (PreviewMediaActivity.Companion.canBePreviewed(mWaitingToPreview)) { + mWaitingToPreview?.let { + startMediaPreview(it, 0, true, true, true, true) + detailsFragmentChanged = true + } + } else if (MimeTypeUtil.isVCard(mWaitingToPreview?.mimeType)) { + startContactListFragment(mWaitingToPreview) + detailsFragmentChanged = true + } else if (PreviewTextFileFragment.canBePreviewed(mWaitingToPreview)) { + startTextPreview(mWaitingToPreview, true) + detailsFragmentChanged = true + } else if (MimeTypeUtil.isPDF(mWaitingToPreview)) { + mWaitingToPreview?.let { + startPdfPreview(it) + detailsFragmentChanged = true + } + } else { + fileOperationsHelper.openFile(mWaitingToPreview) + } + } + mWaitingToPreview = null + } + if (!detailsFragmentChanged) { + leftFragment.updateFileDetails(false, success) + } + } + } + } + + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + if (mDrawerMenuItemstoShowHideList != null) { + val drawerOpen = isDrawerOpen + for (menuItem in mDrawerMenuItemstoShowHideList) { + menuItem.isVisible = !drawerOpen + } + } + + return super.onPrepareOptionsMenu(menu) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + val inflater = menuInflater + inflater.inflate(R.menu.activity_file_display, menu) + + menu.findItem(R.id.action_select_all).isVisible = false + val searchMenuItem = menu.findItem(R.id.action_search) + searchView = MenuItemCompat.getActionView(searchMenuItem) as SearchView? + searchMenuItem.isVisible = false + mSearchText.setOnClickListener { v: View? -> + showSearchView() + searchView?.isIconified = false + } + + searchView?.let { viewThemeUtils.androidx.themeToolbarSearchView(it) } + + // populate list of menu items to show/hide when drawer is opened/closed + mDrawerMenuItemstoShowHideList = ArrayList(1) + mDrawerMenuItemstoShowHideList?.add(searchMenuItem) + + // focus the SearchView + if (!TextUtils.isEmpty(searchQuery)) { + searchView?.post { + searchView?.isIconified = false + searchView?.setQuery(searchQuery, true) + searchView?.clearFocus() + } + } + + val mSearchEditFrame = searchView?.findViewById(androidx.appcompat.R.id.search_edit_frame) + + searchView?.setOnCloseListener { + if (TextUtils.isEmpty(searchView?.query.toString())) { + searchView?.onActionViewCollapsed() + setDrawerIndicatorEnabled(isDrawerIndicatorAvailable) // order matters + supportActionBar?.setDisplayHomeAsUpEnabled(true) + mDrawerToggle.syncState() + + val ocFileListFragment = this.listOfFilesFragment + if (ocFileListFragment != null) { + ocFileListFragment.isSearchFragment = false + ocFileListFragment.refreshDirectory() + } + } else { + searchView?.post { searchView?.setQuery("", true) } + } + true + } + + val vto = mSearchEditFrame?.viewTreeObserver + vto?.addOnGlobalLayoutListener(object : OnGlobalLayoutListener { + var oldVisibility: Int = -1 + + override fun onGlobalLayout() { + val currentVisibility = mSearchEditFrame.visibility + + if (currentVisibility != oldVisibility) { + if (currentVisibility == View.VISIBLE) { + setDrawerIndicatorEnabled(false) + } + + oldVisibility = currentVisibility + } + } + }) + + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + var retval = true + + val itemId = item.itemId + + if (itemId == android.R.id.home) { + if (!isDrawerOpen && + !isSearchOpen() && + isRoot(getCurrentDir()) && + this.leftFragment is OCFileListFragment + ) { + openDrawer() + } else { + onBackPressedDispatcher.onBackPressed() + } + } else if (itemId == R.id.action_select_all) { + val fragment = this.listOfFilesFragment + fragment?.selectAllFiles(true) + } else { + retval = super.onOptionsItemSelected(item) + } + + return retval + } + + /** + * Called, when the user selected something for uploading + */ + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (data != null && + requestCode == REQUEST_CODE__SELECT_CONTENT_FROM_APPS && + (resultCode == RESULT_OK || resultCode == UploadFilesActivity.RESULT_OK_AND_MOVE) + ) { + requestUploadOfContentFromApps(data, resultCode) + } else if (data != null && + requestCode == REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM && + ( + resultCode == RESULT_OK || + resultCode == UploadFilesActivity.RESULT_OK_AND_MOVE || + resultCode == UploadFilesActivity.RESULT_OK_AND_DO_NOTHING || + resultCode == UploadFilesActivity.RESULT_OK_AND_DELETE + ) + ) { + requestUploadOfFilesFromFileSystem(data, resultCode) + } else if (( + requestCode == REQUEST_CODE__UPLOAD_FROM_CAMERA || + requestCode == REQUEST_CODE__UPLOAD_FROM_VIDEO_CAMERA + ) && + (resultCode == RESULT_OK || resultCode == UploadFilesActivity.RESULT_OK_AND_DELETE) + ) { + CheckAvailableSpaceTask( + object : CheckAvailableSpaceListener { + override fun onCheckAvailableSpaceStart() { + Log_OC.d(this, "onCheckAvailableSpaceStart") + } + + override fun onCheckAvailableSpaceFinish( + hasEnoughSpaceAvailable: Boolean, + vararg filesToUpload: String? + ) { + Log_OC.d(this, "onCheckAvailableSpaceFinish") + + if (hasEnoughSpaceAvailable) { + val file = File(filesToUpload[0]) + val renamedFile = if (requestCode == REQUEST_CODE__UPLOAD_FROM_CAMERA) { + File(file.parent + OCFile.PATH_SEPARATOR + FileOperationsHelper.getCapturedImageName()) + } else { + File(file.parent + OCFile.PATH_SEPARATOR + FileOperationsHelper.getCapturedVideoName()) + } + + if (!file.renameTo(renamedFile)) { + DisplayUtils.showSnackMessage( + this@FileDisplayActivity, + R.string.error_uploading_direct_camera_upload + ) + return + } + + requestUploadOfFilesFromFileSystem( + renamedFile.parentFile?.absolutePath, + arrayOf(renamedFile.absolutePath), + FileUploadWorker.LOCAL_BEHAVIOUR_DELETE + ) + } + } + }, + *arrayOf( + FileOperationsHelper.createCameraFile( + this@FileDisplayActivity, + requestCode == REQUEST_CODE__UPLOAD_FROM_VIDEO_CAMERA + ).absolutePath + ) + ).execute() + } else if (requestCode == REQUEST_CODE__MOVE_OR_COPY_FILES && resultCode == RESULT_OK) { + exitSelectionMode() + } else { + super.onActivityResult(requestCode, resultCode, data) + } + } + + private fun exitSelectionMode() { + val ocFileListFragment = this.listOfFilesFragment + ocFileListFragment?.exitSelectionMode() + } + + private fun requestUploadOfFilesFromFileSystem(data: Intent, resultCode: Int) { + val filePaths = data.getStringArrayExtra(UploadFilesActivity.EXTRA_CHOSEN_FILES) ?: return + val basePath = data.getStringExtra(UploadFilesActivity.LOCAL_BASE_PATH) + requestUploadOfFilesFromFileSystem(basePath, filePaths, resultCode) + } + + private fun getRemotePaths(directory: String?, filePaths: Array, localBasePath: String): Array = + Array(filePaths.size) { j -> + val relativePath = StringUtils.removePrefix(filePaths[j], localBasePath) + (directory ?: "") + relativePath + } + + private fun requestUploadOfFilesFromFileSystem(localBasePath: String?, filePaths: Array, resultCode: Int) { + var localBasePath = localBasePath + if (localBasePath != null) { + if (!localBasePath.endsWith("/")) { + localBasePath = "$localBasePath/" + } + + val remotePathBase = getCurrentDir()?.remotePath + val decryptedRemotePaths = getRemotePaths(remotePathBase, filePaths, localBasePath) + + val behaviour = when (resultCode) { + UploadFilesActivity.RESULT_OK_AND_MOVE -> FileUploadWorker.LOCAL_BEHAVIOUR_MOVE + UploadFilesActivity.RESULT_OK_AND_DELETE -> FileUploadWorker.LOCAL_BEHAVIOUR_DELETE + else -> FileUploadWorker.LOCAL_BEHAVIOUR_FORGET + } + + connectivityService.isNetworkAndServerAvailable { result: Boolean? -> + if (result == true) { + val isValidFolderPath = remotePathBase?.let { checkFolderPath(it, capabilities, this) } + if (isValidFolderPath == false) { + DisplayUtils.showSnackMessage( + this, + R.string.file_name_validator_error_contains_reserved_names_or_invalid_characters + ) + return@isNetworkAndServerAvailable + } + + FileUploadHelper.Companion.instance().uploadNewFiles( + user.orElseThrow( + Supplier { RuntimeException() } + ), + filePaths, + decryptedRemotePaths, + behaviour, + true, + UploadFileOperation.CREATED_BY_USER, + false, + false, + NameCollisionPolicy.ASK_USER + ) + } else { + fileDataStorageManager.addCreateFileOfflineOperation(filePaths, decryptedRemotePaths) + } + } + } else { + Log_OC.d(TAG, "User clicked on 'Update' with no selection") + DisplayUtils.showSnackMessage(this, R.string.filedisplay_no_file_selected) + } + } + + private fun requestUploadOfContentFromApps(contentIntent: Intent, resultCode: Int) { + val streamsToUpload = ArrayList() + + if (contentIntent.clipData != null && (contentIntent.clipData?.itemCount ?: 0) > 0) { + for (i in 0..<(contentIntent.clipData?.itemCount ?: 0)) { + streamsToUpload.add(contentIntent.clipData?.getItemAt(i)?.uri) + } + } else { + streamsToUpload.add(contentIntent.data) + } + + val behaviour = + if (resultCode == + UploadFilesActivity.RESULT_OK_AND_MOVE + ) { + FileUploadWorker.LOCAL_BEHAVIOUR_MOVE + } else { + FileUploadWorker.LOCAL_BEHAVIOUR_COPY + } + + val currentDir = getCurrentDir() + val remotePath = if (currentDir != null) currentDir.remotePath else OCFile.ROOT_PATH + + val uploader = UriUploader( + this, + streamsToUpload, + remotePath, + user.orElseThrow( + Supplier { RuntimeException() } + ), + behaviour, + false, // Not show waiting dialog while file is being copied from private storage + null // Not needed copy temp task listener + ) + + uploader.uploadUris() + } + + private fun isSearchOpen(): Boolean { + if (searchView == null) { + return false + } else { + val mSearchEditFrame = searchView?.findViewById(androidx.appcompat.R.id.search_edit_frame) + return mSearchEditFrame != null && mSearchEditFrame.isVisible + } + } + + /** + * Sets up a custom back-press handler for this activity. + * + * This callback determines how the back button behaves based on the current UI state: + * - If the search view is open, it closes it. + * - If the navigation drawer is open, it closes it. + * - If the left fragment is an [OCFileListFragment]: + * - If in the root directory, it either navigates to "All Files" or finishes the activity. + * - Otherwise, it navigates one level up. + * - Otherwise, it pops the current fragment from the back stack. + * + * ### About `isEnabled` + * `isEnabled` is a property of [OnBackPressedCallback]. + * When `isEnabled = false`, this callback is **temporarily disabled**, + * allowing the system or other callbacks to handle the back press instead. + */ + private fun handleBackPress() { + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + when { + isSearchOpen() -> { + isEnabled = false + resetSearchAction() + } + + isDrawerOpen -> { + isEnabled = false + onBackPressedDispatcher.onBackPressed() + } + + leftFragment is OCFileListFragment -> { + val fragment = leftFragment as OCFileListFragment + if (isRoot(getCurrentDir())) { + if (fragment.shouldNavigateBackToAllFiles()) { + navigateToAllFiles() + } else { + finish() + } + } else { + browseUp(fragment) + } + } + + else -> { + isEnabled = false + popBack() + } + } + } + } + ) + } + + private fun browseUp(listOfFiles: OCFileListFragment) { + listOfFiles.onBrowseUp() + val currentFile = listOfFiles.currentFile + + file = currentFile + + currentFile?.let { + listOfFiles.setFabVisible(currentFile.canCreateFileAndFolder()) + listOfFiles.registerFabListener() + } + + resetTitleBarAndScrolling() + configureToolbar() + startMetadataSyncForCurrentDir() + } + + private fun resetSearchAction() { + val leftFragment = this.leftFragment + if (!isSearchOpen() || searchView == null) { + return + } + + searchView?.setQuery("", true) + searchView?.onActionViewCollapsed() + searchView?.clearFocus() + + if (isRoot(getCurrentDir()) && leftFragment is OCFileListFragment) { + // Remove the list to the original state + leftFragment.adapter?.let { adapter -> + val listOfHiddenFiles = adapter.listOfHiddenFiles + leftFragment.performSearch("", listOfHiddenFiles, true) + } + + hideSearchView(getCurrentDir()) + setDrawerIndicatorEnabled(isDrawerIndicatorAvailable) + } + + if (leftFragment is UnifiedSearchFragment) { + showSortListGroup(false) + onBackPressedDispatcher.onBackPressed() + } + } + + /** + * Use this method when want to pop the fragment on back press. It resets Scrolling (See + * [with true][.resetScrolling] and pop the visibility for sortListGroup (See + * [with false][.showSortListGroup]. At last call to onBackPressedDispatcher.onBackPressed() + */ + private fun popBack() { + binding.fabMain.setImageResource(R.drawable.ic_plus) + resetScrolling(true) + showSortListGroup(false) + onBackPressedDispatcher.onBackPressed() + } + + override fun onSaveInstanceState(outState: Bundle) { + // responsibility of restore is preferred in onCreate() before than in + // onRestoreInstanceState when there are Fragments involved + super.onSaveInstanceState(outState) + mWaitingToPreview.logFileSize(TAG) + outState.putParcelable(KEY_WAITING_TO_PREVIEW, mWaitingToPreview) + outState.putBoolean(KEY_SYNC_IN_PROGRESS, mSyncInProgress) + // outState.putBoolean(FileDisplayActivity.KEY_REFRESH_SHARES_IN_PROGRESS, + // mRefreshSharesInProgress); + outState.putParcelable(KEY_WAITING_TO_SEND, mWaitingToSend) + if (searchView != null) { + outState.putBoolean(KEY_IS_SEARCH_OPEN, searchView?.isIconified == false) + } + outState.putString(KEY_SEARCH_QUERY, searchQuery) + outState.putBoolean(KEY_IS_SORT_GROUP_VISIBLE, sortListGroupVisibility()) + Log_OC.v(TAG, "onSaveInstanceState() end") + } + + override fun onResume() { + Log_OC.v(TAG, "onResume() start") + super.onResume() + isFileDisplayActivityResumed = true + + // Instead of onPostCreate, starting the loading in onResume for children fragments + val leftFragment = this.leftFragment + + // Listen for sync messages + if (leftFragment !is OCFileListFragment || !leftFragment.isSearchFragment) { + initSyncBroadcastReceiver() + } + + if (leftFragment !is OCFileListFragment) { + if (leftFragment is FileFragment) { + super.updateActionBarTitleAndHomeButton(leftFragment.file) + } + return + } + + val ocFileListFragment = leftFragment + syncAndUpdateFolder(ignoreETag = true, ignoreFocus = true) + + // Try to get the OCFile from the intent, if one was provided when launching this activity. + // 'file' comes from the FileActivity base class and represents the currently opened file or folder. + // We update it only when a valid file is found in the intent. + val startFile = intent?.let { getFileFromIntent(it) }?.also { + file = it + } + + // refresh list of files + if (searchView != null && !TextUtils.isEmpty(searchQuery)) { + searchView?.setQuery(searchQuery, false) + } else if (!ocFileListFragment.isSearchFragment && startFile == null) { + updateListOfFilesFragment(false) + ocFileListFragment.registerFabListener() + } else { + ocFileListFragment.listDirectory(startFile, false, false) + updateActionBarTitleAndHomeButton(startFile) + } + + // Listen for upload messages + val uploadIntentFilter = IntentFilter(getUploadFinishMessage()) + mUploadFinishReceiver = UploadFinishReceiver() + localBroadcastManager.registerReceiver(mUploadFinishReceiver!!, uploadIntentFilter) + + // Listen for download messages + val downloadIntentFilter = IntentFilter(getDownloadAddedMessage()) + downloadIntentFilter.addAction(getDownloadFinishMessage()) + mDownloadFinishReceiver = DownloadFinishReceiver() + mDownloadFinishReceiver?.let { + localBroadcastManager.registerReceiver(it, downloadIntentFilter) + } + + configureToolbar() + + // show in-app review dialog to user + inAppReviewHelper.showInAppReview(this) + + checkNotifications() + + Log_OC.v(TAG, "onResume() end") + + Handler(Looper.getMainLooper()).postDelayed({ + isFileDisplayActivityResumed = false + }, ON_RESUMED_RESET_DELAY) + } + + private fun getFileFromIntent(intent: Intent?): OCFile? = + intent.getParcelableArgument(EXTRA_FILE, OCFile::class.java) + ?: intent?.getStringExtra(EXTRA_FILE_REMOTE_PATH) + ?.let { fileDataStorageManager.getFileByDecryptedRemotePath(it) } + + private fun checkAndSetMenuItemId() { + if (MainApp.isOnlyPersonFiles()) { + menuItemId = R.id.nav_personal_files + } else if (MainApp.isOnlyOnDevice()) { + menuItemId = R.id.nav_on_device + } else if (menuItemId == Menu.NONE) { + menuItemId = R.id.nav_all_files + } + } + + private fun configureToolbar() { + checkAndSetMenuItemId() + setNavigationViewItemChecked() + val item = ToolbarItem.fromNavId(menuItemId) + when (item?.style) { + ToolbarStyle.SEARCH -> setupHomeSearchToolbarWithSortAndListButtons() + ToolbarStyle.PLAIN -> { + if (currentDir?.isRootDirectory == true) { + updateActionBarTitleAndHomeButtonByString(getString(item.titleId)) + } else { + setupToolbar() + } + } + else -> { + setupToolbar() + } + } + } + + fun initSyncBroadcastReceiver() { + if (mSyncBroadcastReceiver == null) { + val syncIntentFilter = IntentFilter(FileSyncAdapter.EVENT_FULL_SYNC_START).apply { + addAction(FileSyncAdapter.EVENT_FULL_SYNC_END) + addAction(FileSyncAdapter.EVENT_FULL_SYNC_FOLDER_CONTENTS_SYNCED) + addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED) + addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED) + } + + mSyncBroadcastReceiver = SyncBroadcastReceiver() + mSyncBroadcastReceiver?.let { + localBroadcastManager.registerReceiver(it, syncIntentFilter) + } + } + } + + override fun onPause() { + Log_OC.v(TAG, "onPause() start") + if (mSyncBroadcastReceiver != null) { + localBroadcastManager.unregisterReceiver(mSyncBroadcastReceiver!!) + mSyncBroadcastReceiver = null + } + if (mUploadFinishReceiver != null) { + localBroadcastManager.unregisterReceiver(mUploadFinishReceiver!!) + mUploadFinishReceiver = null + } + if (mDownloadFinishReceiver != null) { + localBroadcastManager.unregisterReceiver(mDownloadFinishReceiver!!) + mDownloadFinishReceiver = null + } + + super.onPause() + Log_OC.v(TAG, "onPause() end") + } + + override fun onSortingOrderChosen(selection: FileSortOrder?) { + val ocFileListFragment = this.listOfFilesFragment + ocFileListFragment?.sortFiles(selection) + } + + override fun downloadFile(file: OCFile?, packageName: String?, activityName: String?) { + if (packageName != null && activityName != null) { + startDownloadForSending(file, OCFileListFragment.DOWNLOAD_SEND, packageName, activityName) + } + } + + // region SyncBroadcastReceiver + private inner class SyncBroadcastReceiver : BroadcastReceiver() { + @SuppressLint("VisibleForTests") + override fun onReceive(context: Context?, intent: Intent) { + try { + val event = intent.action + Log_OC.d(TAG, "Received broadcast $event") + + // region EventData + val accountName = intent.getStringExtra(FileSyncAdapter.EXTRA_ACCOUNT_NAME) + val syncFolderRemotePath = intent.getStringExtra(FileSyncAdapter.EXTRA_FOLDER_PATH) + val id = intent.getStringExtra(FileSyncAdapter.EXTRA_RESULT) + val syncResult = DataHolderUtil.getInstance().retrieve(id) + val sameAccount = + account != null && accountName != null && accountName == account.name && storageManager != null + val fileListFragment: OCFileListFragment? = this@FileDisplayActivity.listOfFilesFragment + + // endregion + if (sameAccount) { + handleSyncEvent(event, syncFolderRemotePath, id, fileListFragment, syncResult) + } + + if (syncResult is RemoteOperationResult<*> && + syncResult.code == RemoteOperationResult.ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED + ) { + mLastSslUntrustedServerResult = syncResult + } + } catch (_: java.lang.RuntimeException) { + safelyDeleteResult(intent) + } finally { + mSyncInProgress = false + } + } + } + + // avoid app crashes after changing the serial id of RemoteOperationResult in owncloud library + // with broadcast notifications pending to process + private fun safelyDeleteResult(intent: Intent) { + try { + DataHolderUtil.getInstance().delete(intent.getStringExtra(FileSyncAdapter.EXTRA_RESULT)) + } catch (_: java.lang.RuntimeException) { + Log_OC.i(TAG, "Ignoring error deleting data") + } + } + + private fun handleSyncEvent( + event: String?, + syncFolderRemotePath: String?, + id: String?, + fileListFragment: OCFileListFragment?, + syncResult: Any? + ) { + if (FileSyncAdapter.EVENT_FULL_SYNC_START == event) { + mSyncInProgress = true + return + } + + var currentFile = file?.remotePath?.let { storageManager.getFileByPath(it) } + val currentDir = getCurrentDir()?.remotePath?.let { storageManager.getFileByPath(it) } + val isSyncFolderRemotePathRoot = OCFile.ROOT_PATH == syncFolderRemotePath + + if (currentDir == null && !isSyncFolderRemotePathRoot) { + handleRemovedFolder(syncFolderRemotePath) + } else if (currentDir != null) { + currentFile = handleRemovedFileFromServer(currentFile, currentDir) + updateFileList(fileListFragment, currentDir, syncFolderRemotePath) + file = currentFile + } + + handleSyncResult(event, syncResult) + + DataHolderUtil.getInstance().delete(id) + + mSyncInProgress = + FileSyncAdapter.EVENT_FULL_SYNC_END != event && + RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED != event + Log_OC.d(TAG, "Setting progress visibility to $mSyncInProgress") + + handleScrollBehaviour(fileListFragment) + } + + private fun handleRemovedFileFromServer(currentFile: OCFile?, currentDir: OCFile?): OCFile? { + if (currentFile == null && file?.isFolder == false) { + resetTitleBarAndScrolling() + return currentDir + } + + return currentFile + } + + private fun handleRemovedFolder(syncFolderRemotePath: String?) { + DisplayUtils.showSnackMessage(this, R.string.sync_current_folder_was_removed, syncFolderRemotePath) + browseToRoot() + } + + private fun updateFileList( + ocFileListFragment: OCFileListFragment?, + currentDir: OCFile, + syncFolderRemotePath: String? + ) { + if (currentDir.remotePath != syncFolderRemotePath) { + return + } + + if (ocFileListFragment == null) { + return + } + + ocFileListFragment.listDirectory(currentDir, MainApp.isOnlyOnDevice(), false) + } + + private fun handleScrollBehaviour(ocFileListFragment: OCFileListFragment?) { + if (ocFileListFragment == null) { + return + } + + if (mSyncInProgress || ocFileListFragment.isLoading) { + return + } + + if (ocFileListFragment.isEmpty) { + lockScrolling() + return + } + + resetScrolling(false) + } + + private fun handleSyncResult(event: String?, syncResult: Any?) { + if (RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED != event || syncResult == null) { + return + } + + if (syncResult is RemoteOperationResult<*> && syncResult.isSuccess) { + hideInfoBox() + return + } + + handleFailedSyncResult(syncResult) + } + + private fun handleFailedSyncResult(syncResult: Any?) { + if (checkForRemoteOperationError(syncResult)) { + requestCredentialsUpdate() + } else { + handleNonCredentialSyncErrors(syncResult) + } + } + + private fun handleNonCredentialSyncErrors(syncResult: Any?) { + if (syncResult !is RemoteOperationResult<*>) { + return + } + + when (syncResult.code) { + RemoteOperationResult.ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED -> showUntrustedCertDialog(syncResult) + RemoteOperationResult.ResultCode.MAINTENANCE_MODE -> showInfoBox(R.string.maintenance_mode) + RemoteOperationResult.ResultCode.NO_NETWORK_CONNECTION -> showInfoBox(R.string.offline_mode) + RemoteOperationResult.ResultCode.HOST_NOT_AVAILABLE -> showInfoBox(R.string.host_not_available) + RemoteOperationResult.ResultCode.SIGNING_TOS_NEEDED -> showTermsOfServiceDialog() + else -> {} + } + } + + private fun showTermsOfServiceDialog() { + if (supportFragmentManager.findFragmentByTag(DIALOG_TAG_SHOW_TOS) == null) { + TermsOfServiceDialog().show(supportFragmentManager, DIALOG_TAG_SHOW_TOS) + } + } + + private fun checkForRemoteOperationError(syncResult: Any?): Boolean { + if (syncResult !is RemoteOperationResult<*>) { + return false + } + + return RemoteOperationResult.ResultCode.UNAUTHORIZED == syncResult.code || + (syncResult.isException && syncResult.exception is AuthenticatorException) + } + + /** + * Show a text message on screen view for notifying user if content is loading or folder is empty + */ + private fun setBackgroundText() { + val ocFileListFragment = listOfFilesFragment ?: return + connectivityService.isNetworkAndServerAvailable { result: Boolean? -> + when { + mSyncInProgress && result == true -> { + ocFileListFragment.setEmptyListMessage(EmptyListState.LOADING) + } + MainApp.isOnlyOnDevice() -> { + ocFileListFragment.setEmptyListMessage(EmptyListState.ONLY_ON_DEVICE) + } + result == true -> ocFileListFragment.setEmptyListMessage(SearchType.NO_SEARCH) + else -> ocFileListFragment.setEmptyListMessage(EmptyListState.OFFLINE_MODE) + } + } + } + + // endregion + /** + * Once the file upload has finished -> update view + */ + private inner class UploadFinishReceiver : BroadcastReceiver() { + /** + * Once the file upload has finished -> update view + * + * + * [BroadcastReceiver] to enable upload feedback in UI + */ + override fun onReceive(context: Context?, intent: Intent) { + val uploadedRemotePath = intent.getStringExtra(FileUploadWorker.EXTRA_REMOTE_PATH) + val accountName = intent.getStringExtra(FileUploadWorker.ACCOUNT_NAME) + val account = getAccount() + val sameAccount = accountName != null && account != null && accountName == account.name + val currentDir = getCurrentDir() + val isDescendant = + currentDir != null && uploadedRemotePath != null && uploadedRemotePath.startsWith(currentDir.remotePath) + + if (sameAccount && isDescendant) { + val linkedToRemotePath = intent.getStringExtra(FileUploadWorker.EXTRA_LINKED_TO_PATH) + if (linkedToRemotePath == null || isAscendant(linkedToRemotePath)) { + updateListOfFilesFragment(false) + } + } + + val uploadWasFine = intent.getBooleanExtra(FileUploadWorker.EXTRA_UPLOAD_RESULT, false) + + var renamedInUpload = false + var sameFile = false + if (file != null) { + renamedInUpload = + file?.remotePath == intent.getStringExtra(FileUploadWorker.EXTRA_OLD_REMOTE_PATH) + sameFile = file?.remotePath == uploadedRemotePath || renamedInUpload + } + + if (sameAccount && sameFile && this@FileDisplayActivity.leftFragment is FileDetailFragment) { + val fileDetailFragment = leftFragment as FileDetailFragment + if (uploadWasFine) { + file = storageManager.getFileByPath(uploadedRemotePath) + } else { + // TODO remove upload progress bar after upload failed. + Log_OC.d(TAG, "Remove upload progress bar after upload failed") + } + if (renamedInUpload && !uploadedRemotePath.isNullOrBlank()) { + val newName = File(uploadedRemotePath).name + DisplayUtils.showSnackMessage( + this@FileDisplayActivity, + R.string.filedetails_renamed_in_upload_msg, + newName + ) + } + + if (uploadWasFine || file?.fileExists() == true) { + fileDetailFragment.updateFileDetails(false, true) + } else { + onBackPressedDispatcher.onBackPressed() + } + + // Force the preview if the file is an image or text file + if (uploadWasFine) { + file?.let { + if (PreviewImageFragment.canBePreviewed(it)) { + startImagePreview(it, true) + } else if (PreviewTextFileFragment.canBePreviewed(it)) { + startTextPreview(it, true) + } + } + } + } + } + + // TODO refactor this receiver, and maybe DownloadFinishReceiver; this method is duplicated :S + fun isAscendant(linkedToRemotePath: String): Boolean { + val currentDir = getCurrentDir() + return currentDir != null && currentDir.remotePath.startsWith(linkedToRemotePath) + } + } + + /** + * Class waiting for broadcast events from the [FileDownloadWorker] service. + * + * + * Updates the UI when a download is started or finished, provided that it is relevant for the current folder. + */ + private inner class DownloadFinishReceiver : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent) { + val sameAccount = isSameAccount(intent) + val downloadedRemotePath = intent.getStringExtra(FileDownloadWorker.EXTRA_REMOTE_PATH) + val downloadBehaviour = intent.getStringExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR) + val isDescendant = isDescendant(downloadedRemotePath) + + if (sameAccount && isDescendant) { + val linkedToRemotePath = intent.getStringExtra(FileDownloadWorker.EXTRA_LINKED_TO_PATH) + if (linkedToRemotePath == null || isAscendant(linkedToRemotePath)) { + updateListOfFilesFragment(false) + } + + val intentAction = intent.action + if (intentAction != null && downloadedRemotePath != null) { + refreshDetailsFragmentIfVisible( + intentAction, + downloadedRemotePath, + intent.getBooleanExtra(FileDownloadWorker.EXTRA_DOWNLOAD_RESULT, false) + ) + } + } + + if (mWaitingToSend != null) { + // update file after downloading + mWaitingToSend = storageManager.getFileByRemoteId(mWaitingToSend?.remoteId) + if (mWaitingToSend != null && + mWaitingToSend?.isDown == true && + OCFileListFragment.DOWNLOAD_SEND == downloadBehaviour + ) { + val packageName = intent.getStringExtra(SendShareDialog.PACKAGE_NAME) ?: return + val activityName = intent.getStringExtra(SendShareDialog.ACTIVITY_NAME) ?: return + sendDownloadedFile(packageName, activityName) + } + } + + if (mWaitingToPreview != null) { + mWaitingToPreview = storageManager.getFileByRemoteId(mWaitingToPreview?.remoteId) + if (mWaitingToPreview != null && + mWaitingToPreview?.isDown == true && + EditImageActivity.OPEN_IMAGE_EDITOR == downloadBehaviour + ) { + mWaitingToPreview?.let { + startImageEditor(it) + } + } + } + } + + fun isDescendant(downloadedRemotePath: String?): Boolean { + val currentDir = getCurrentDir() + return currentDir != null && + downloadedRemotePath != null && + downloadedRemotePath.startsWith(currentDir.remotePath) + } + + fun isAscendant(linkedToRemotePath: String): Boolean { + val currentDir = getCurrentDir() + return currentDir != null && currentDir.remotePath.startsWith(linkedToRemotePath) + } + + fun isSameAccount(intent: Intent): Boolean { + val accountName = intent.getStringExtra(FileDownloadWorker.EXTRA_ACCOUNT_NAME) + return accountName != null && account != null && accountName == account.name + } + } + + fun browseToRoot() { + val listOfFiles = this.listOfFilesFragment + if (listOfFiles != null) { // should never be null, indeed + val root = storageManager.getFileByPath(OCFile.ROOT_PATH) + listOfFiles.listDirectory(root, MainApp.isOnlyOnDevice(), false) + file = listOfFiles.currentFile + startSyncFolderOperation(root, false) + } + binding.fabMain.setImageResource(R.drawable.ic_plus) + resetTitleBarAndScrolling() + } + + override fun onBrowsedDownTo(directory: OCFile?) { + file = directory + resetTitleBarAndScrolling() + startSyncFolderOperation(directory, false) + startMetadataSyncForCurrentDir() + } + + /** + * Shows the information of the [OCFile] received as a parameter. + * + * @param file [OCFile] whose details will be shown + */ + override fun showDetails(file: OCFile?) { + showDetails(file, 0) + } + + /** + * Shows the information of the [OCFile] received as a parameter. + * + * @param file [OCFile] whose details will be shown + * @param activeTab the active tab in the details view + */ + override fun showDetails(file: OCFile?, activeTab: Int) { + val currentUser = user.orElseThrow(Supplier { RuntimeException() }) + + resetScrolling(true) + + val detailFragment: Fragment = FileDetailFragment.newInstance(file, currentUser, activeTab) + setLeftFragment(detailFragment, false) + configureToolbarForPreview(file) + } + + /** + * Prevents content scrolling and toolbar collapse + */ + @VisibleForTesting + fun lockScrolling() { + binding.appbar.appbar.setExpanded(true, false) + val appbarParams = binding.appbar.toolbarFrame.layoutParams as AppBarLayout.LayoutParams + appbarParams.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL) + binding.appbar.toolbarFrame.layoutParams = appbarParams + } + + /** + * Resets content scrolling and toolbar collapse + */ + @VisibleForTesting + fun resetScrolling(expandAppBar: Boolean) { + val appbarParams = binding.appbar.toolbarFrame.layoutParams as AppBarLayout.LayoutParams + appbarParams.setScrollFlags( + AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS + ) + binding.appbar.toolbarFrame.layoutParams = appbarParams + if (expandAppBar) { + binding.appbar.appbar.setExpanded(true, false) + } + } + + public override fun updateActionBarTitleAndHomeButton(chosenFile: OCFile?) { + var chosenFile = chosenFile + if (chosenFile == null) { + chosenFile = file // if no file is passed, current file decides + } + super.updateActionBarTitleAndHomeButton(chosenFile) + } + + override fun isDrawerIndicatorAvailable(): Boolean = isRoot(getCurrentDir()) + + private fun observeWorkerState() { + WorkerStateLiveData.Companion.instance().observe( + this, + Observer { state: WorkerState? -> + when (state) { + is DownloadStarted -> { + Log_OC.d(TAG, "Download worker started") + handleDownloadWorkerState() + } + + is DownloadFinished -> { + fileDownloadProgressListener = null + previewFile(state) + } + + is UploadFinished -> { + refreshList() + } + + is OfflineOperationsCompleted -> { + refreshCurrentDirectory() + } + + else -> { + } + } + } + ) + } + + private fun previewFile(finishedState: DownloadFinished) { + if (fileIDForImmediatePreview == -1L) { + return + } + + val currentFile = finishedState.currentFile + if (currentFile == null) { + return + } + + if (fileIDForImmediatePreview != currentFile.fileId || !currentFile.isDown) { + return + } + + fileIDForImmediatePreview = -1 + if (PreviewImageFragment.canBePreviewed(currentFile)) { + startImagePreview(currentFile, currentFile.isDown) + } else { + previewFile(currentFile, null) + } + } + + fun previewImageWithSearchContext(file: OCFile, searchFragment: Boolean, currentSearchType: SearchType?) { + // preview image - it handles the download, if needed + if (searchFragment) { + val type = when (currentSearchType) { + SearchType.FAVORITE_SEARCH -> VirtualFolderType.FAVORITE + SearchType.GALLERY_SEARCH -> VirtualFolderType.GALLERY + else -> VirtualFolderType.NONE + } + + startImagePreview(file, type, file.isDown) + } else { + startImagePreview(file, file.isDown) + } + } + + fun previewFile(file: OCFile, setFabVisible: CompletionCallback?) { + if (!file.isDown) { + Log_OC.d(TAG, "File is not downloaded, cannot be previewed") + return + } + + if (MimeTypeUtil.isVCard(file)) { + startContactListFragment(file) + } else if (MimeTypeUtil.isPDF(file)) { + startPdfPreview(file) + } else if (PreviewTextFileFragment.canBePreviewed(file)) { + setFabVisible?.onComplete(false) + startTextPreview(file, false) + } else if (PreviewMediaActivity.Companion.canBePreviewed(file)) { + setFabVisible?.onComplete(false) + startMediaPreview(file, 0, true, true, false, true) + } else { + fileOperationsHelper.openFile(file) + } + } + + fun refreshCurrentDirectory() { + val currentDir = + if (getCurrentDir() != + null + ) { + storageManager.getFileByDecryptedRemotePath(getCurrentDir()?.remotePath) + } else { + null + } + + val lastFragment = lastFragment() + + var fileListFragment: OCFileListFragment? = null + if (lastFragment is OCFileListFragment) { + fileListFragment = lastFragment + } + if (fileListFragment == null) { + fileListFragment = listOfFilesFragment + } + fileListFragment?.listDirectory(currentDir, MainApp.isOnlyOnDevice(), false) + } + + private fun handleDownloadWorkerState() { + if (mWaitingToPreview != null && storageManager != null) { + mWaitingToPreview = mWaitingToPreview?.fileId?.let { storageManager.getFileById(it) } + if (mWaitingToPreview != null && mWaitingToPreview?.isDown == false) { + requestForDownload() + } + } + } + + override fun newTransferenceServiceConnection(): ServiceConnection = ListServiceConnection() + + /** + * Defines callbacks for service binding, passed to bindService() + * TODO: Check if this can be removed since download and uploads uses work manager now. + */ + private inner class ListServiceConnection : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) = Unit + + override fun onServiceDisconnected(component: ComponentName) { + if (component == ComponentName(this@FileDisplayActivity, FileDownloadWorker::class.java)) { + Log_OC.d(TAG, "Download service disconnected") + fileDownloadProgressListener = null + } + } + } + + /** + * Updates the view associated to the activity after the finish of some operation over files in the current + * account. + * + * @param operation Removal operation performed. + * @param result Result of the removal. + */ + override fun onRemoteOperationFinish(operation: RemoteOperation<*>?, result: RemoteOperationResult<*>) { + super.onRemoteOperationFinish(operation, result) + + when (operation) { + is RemoveFileOperation -> { + onRemoveFileOperationFinish(operation, result) + } + + is RenameFileOperation -> { + onRenameFileOperationFinish(operation, result) + } + + is SynchronizeFileOperation -> { + onSynchronizeFileOperationFinish(operation, result) + } + + is CreateFolderOperation -> { + onCreateFolderOperationFinish(operation, result) + } + + is MoveFileOperation -> { + onMoveFileOperationFinish(operation, result) + } + + is CopyFileOperation -> { + onCopyFileOperationFinish(operation, result) + } + + is RestoreFileVersionRemoteOperation -> { + onRestoreFileVersionOperationFinish(result) + } + } + } + + private val fileListFragment: OCFileListFragment? + get() = if (lastFragment() is OCFileListFragment) lastFragment() as OCFileListFragment else listOfFilesFragment + + private fun refreshGalleryFragmentIfNeeded() { + val fileListFragment = this.fileListFragment + if (fileListFragment is GalleryFragment) { + startPhotoSearch(R.id.nav_gallery) + } + } + + private fun refreshShowDetails() { + val details = this.leftFragment + if (details is FileFragment) { + var file = details.file + if (file != null) { + file = storageManager.getFileByPath(file.remotePath) + if (details is PreviewTextFragment) { + // Refresh OCFile of the fragment + (details as PreviewTextFileFragment).updateFile(file) + } else { + showDetails(file) + } + } + supportInvalidateOptionsMenu() + } + } + + /** + * Updates the view associated to the activity after the finish of an operation trying to remove a file. + * + * @param operation Removal operation performed. + * @param result Result of the removal. + */ + private fun onRemoveFileOperationFinish(operation: RemoveFileOperation, result: RemoteOperationResult<*>) { + if (!operation.isInBackground) { + DisplayUtils.showSnackMessage( + this, + ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()) + ) + } + + if (result.isSuccess) { + val removedFile = operation.file + tryStopPlaying(removedFile) + val leftFragment = this.leftFragment + + // check if file is still available, if so do nothing + val fileAvailable = storageManager.fileExists(removedFile.fileId) + if (leftFragment is FileFragment && !fileAvailable && removedFile == leftFragment.file) { + file = storageManager.getFileById(removedFile.parentId) + resetTitleBarAndScrolling() + } + val parentFile = storageManager.getFileById(removedFile.parentId) + if (parentFile != null && parentFile == getCurrentDir()) { + updateListOfFilesFragment(false) + } else if (this.leftFragment is GalleryFragment) { + val galleryFragment = leftFragment as GalleryFragment + galleryFragment.onRefresh() + } else if (leftFragment is OCFileListFragment && + SearchRemoteOperation.SearchType.FAVORITE_SEARCH == leftFragment.searchEvent?.searchType + ) { + leftFragment.adapter?.run { + val file = files.find { it.fileId == removedFile.fileId } + if (file != null) { + val pos = getItemPosition(file) + files.remove(file) + notifyItemRemoved(pos) + } + } + } + supportInvalidateOptionsMenu() + refreshGalleryFragmentIfNeeded() + fetchRecommendedFilesIfNeeded(ignoreETag = true, currentDir) + } else { + if (result.isSslRecoverableException) { + mLastSslUntrustedServerResult = result + showUntrustedCertDialog(mLastSslUntrustedServerResult) + } + } + } + + private fun onRestoreFileVersionOperationFinish(result: RemoteOperationResult<*>) { + if (result.isSuccess) { + val file = getFile() + + // delete old local copy + if (file?.isDown == true) { + val list: MutableList = ArrayList() + list.add(file) + fileOperationsHelper.removeFiles(list, true, true) + + // download new version, only if file was previously download + showSyncLoadingDialog(file.isFolder == true) + fileOperationsHelper.syncFile(file) + } + + val parent = file?.let { storageManager.getFileById(it.parentId) } + startSyncFolderOperation(parent, ignoreETag = true, ignoreFocus = true) + + val leftFragment = this.leftFragment + if (leftFragment is FileDetailFragment) { + leftFragment.getFileDetailActivitiesFragment().reload() + } + + DisplayUtils.showSnackMessage(this, R.string.file_version_restored_successfully) + } else { + DisplayUtils.showSnackMessage(this, R.string.file_version_restored_error) + } + } + + private fun tryStopPlaying(file: OCFile) { + // placeholder for stop-on-delete future code + if (mPlayerConnection != null && MimeTypeUtil.isAudio(file) && mPlayerConnection?.isPlaying() == true) { + mPlayerConnection?.stop(file) + } + } + + /** + * Updates the view associated to the activity after the finish of an operation trying to move a file. + * + * @param operation Move operation performed. + * @param result Result of the move operation. + */ + private fun onMoveFileOperationFinish(operation: MoveFileOperation?, result: RemoteOperationResult<*>) { + if (!result.isSuccess) { + try { + DisplayUtils.showSnackMessage( + this, + ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()) + ) + } catch (e: Resources.NotFoundException) { + Log_OC.e(TAG, "Error while trying to show fail message ", e) + } + } + } + + /** + * Updates the view associated to the activity after the finish of an operation trying to copy a file. + * + * @param operation Copy operation performed. + * @param result Result of the copy operation. + */ + private fun onCopyFileOperationFinish(operation: CopyFileOperation?, result: RemoteOperationResult<*>) { + if (result.isSuccess) { + updateListOfFilesFragment(false) + refreshGalleryFragmentIfNeeded() + } else { + try { + DisplayUtils.showSnackMessage( + this, + ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()) + ) + } catch (e: Resources.NotFoundException) { + Log_OC.e(TAG, "Error while trying to show fail message ", e) + } + } + } + + /** + * Updates the view associated to the activity after the finish of an operation trying to rename a file. + * + * @param operation Renaming operation performed. + * @param result Result of the renaming. + */ + private fun onRenameFileOperationFinish(operation: RenameFileOperation, result: RemoteOperationResult<*>) { + val optionalUser = user + val renamedFile = operation.file + if (result.isSuccess && optionalUser.isPresent) { + val currentUser = optionalUser.get() + val leftFragment = this.leftFragment + if (leftFragment is FileFragment) { + if (leftFragment is FileDetailFragment && renamedFile == leftFragment.file) { + leftFragment.updateFileDetails(renamedFile, currentUser) + showDetails(renamedFile) + } else if (leftFragment is PreviewMediaFragment && renamedFile == leftFragment.file) { + leftFragment.updateFile(renamedFile) + if (PreviewMediaFragment.canBePreviewed(renamedFile)) { + val position = leftFragment.position + startMediaPreview(renamedFile, position, true, true, true, false) + } else { + fileOperationsHelper.openFile(renamedFile) + } + } else if (leftFragment is PreviewTextFragment && renamedFile == leftFragment.file) { + (leftFragment as PreviewTextFileFragment).updateFile(renamedFile) + if (PreviewTextFileFragment.canBePreviewed(renamedFile)) { + startTextPreview(renamedFile, true) + } else { + fileOperationsHelper.openFile(renamedFile) + } + } + } + + val file = storageManager.getFileById(renamedFile.parentId) + if (file != null && file == getCurrentDir()) { + updateListOfFilesFragment(false) + } + refreshGalleryFragmentIfNeeded() + fetchRecommendedFilesIfNeeded(ignoreETag = true, currentDir) + } else { + DisplayUtils.showSnackMessage( + this, + ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()) + ) + + if (result.isSslRecoverableException) { + mLastSslUntrustedServerResult = result + showUntrustedCertDialog(mLastSslUntrustedServerResult) + } + } + } + + private fun onSynchronizeFileOperationFinish( + operation: SynchronizeFileOperation, + result: RemoteOperationResult<*> + ) { + if (result.isSuccess && operation.transferWasRequested()) { + val syncedFile = operation.localFile + onTransferStateChanged(syncedFile, true, true) + supportInvalidateOptionsMenu() + refreshShowDetails() + } + } + + /** + * Updates the view associated to the activity after the finish of an operation trying create a new folder + * + * @param operation Creation operation performed. + * @param result Result of the creation. + */ + private fun onCreateFolderOperationFinish(operation: CreateFolderOperation, result: RemoteOperationResult<*>) { + if (result.isSuccess) { + val fileListFragment = this.listOfFilesFragment + fileListFragment?.onItemClicked(storageManager.getFileByDecryptedRemotePath(operation.getRemotePath())) + } else { + try { + if (RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS == result.code) { + DisplayUtils.showSnackMessage(this, R.string.folder_already_exists) + } else { + DisplayUtils.showSnackMessage( + this, + ErrorMessageAdapter.getErrorCauseMessage(result, operation, getResources()) + ) + } + } catch (e: Resources.NotFoundException) { + Log_OC.e(TAG, "Error while trying to show fail message ", e) + } + } + } + + /** + * {@inheritDoc} + */ + override fun onTransferStateChanged(file: OCFile, downloading: Boolean, uploading: Boolean) { + updateListOfFilesFragment(false) + val leftFragment = this.leftFragment + val optionalUser = user + if (leftFragment is FileDetailFragment && file == leftFragment.file && optionalUser.isPresent) { + val currentUser = optionalUser.get() + if (downloading || uploading) { + leftFragment.updateFileDetails(file, currentUser) + } else { + if (!file.fileExists()) { + resetTitleBarAndScrolling() + } else { + leftFragment.updateFileDetails(false, true) + } + } + } + } + + private fun requestForDownload() { + val user = user.orElseThrow(Supplier { RuntimeException() }) + mWaitingToPreview?.let { + FileDownloadHelper.Companion.instance().downloadFileIfNotStartedBefore(user, it) + } + } + + override fun onSavedCertificate() { + startSyncFolderOperation(getCurrentDir(), false) + } + + /** + * Starts an operation to refresh the requested folder. + * + * + * The operation is run in a new background thread created on the fly. + * + * + * The refresh updates is a "light sync": properties of regular files in folder are updated (including associated + * shares), but not their contents. Only the contents of files marked to be kept-in-sync are synchronized too. + * + * @param folder Folder to refresh. + * @param ignoreETag If 'true', the data from the server will be fetched and sync'ed even if the eTag didn't + * change. + * @param ignoreFocus reloads file list even without focus, e.g. on tablet mode, focus can still be in detail view + */ + /** + * Starts an operation to refresh the requested folder. + * + * + * The operation is run in a new background thread created on the fly. + * + * + * The refresh updates is a "light sync": properties of regular files in folder are updated (including associated + * shares), but not their contents. Only the contents of files marked to be kept-in-sync are synchronized too. + * + * @param folder Folder to refresh. + * @param ignoreETag If 'true', the data from the server will be fetched and sync'ed even if the eTag didn't + * change. + */ + @JvmOverloads + fun startSyncFolderOperation(folder: OCFile?, ignoreETag: Boolean, ignoreFocus: Boolean = false) { + Log_OC.d(TAG, "startSyncFolderOperation called, ignoreEtag: $ignoreETag, ignoreFocus: $ignoreFocus") + + // the execution is slightly delayed to allow the activity get the window focus if it's being started + // or if the method is called from a dialog that is being dismissed + + if (TextUtils.isEmpty(searchQuery) && user.isPresent) { + handler.postDelayed({ + val user = getUser() + if (!ignoreFocus && !hasWindowFocus() || !user.isPresent) { + // do not refresh if the user rotates the device while another window has focus + // or if the current user is no longer valid + return@postDelayed + } + + val currentSyncTime = System.currentTimeMillis() + mSyncInProgress = true + + // perform folder synchronization + val refreshFolderOperation: RemoteOperation<*> = RefreshFolderOperation( + folder, + currentSyncTime, + false, + ignoreETag, + storageManager, + user.get(), + applicationContext + ) + refreshFolderOperation.execute( + account, + MainApp.getAppContext(), + this@FileDisplayActivity, + null, + null + ) + + fetchRecommendedFilesIfNeeded(ignoreETag, folder) + mSyncInProgress = false + ocFileListFragment?.setLoading(false) + }, DELAY_TO_REQUEST_REFRESH_OPERATION_LATER) + } + } + + private fun fetchRecommendedFilesIfNeeded(ignoreETag: Boolean, folder: OCFile?) { + if (folder?.isRootDirectory == false || capabilities == null || capabilities.recommendations.isFalse) { + return + } + + if (user.isPresent) { + val accountName = user.get().accountName + val fragment = this.listOfFilesFragment + lifecycleScope.launch(Dispatchers.IO) { + val recommendedFiles = filesRepository.fetchRecommendedFiles(accountName, ignoreETag, storageManager) + withContext(Dispatchers.Main) { + fragment?.adapter?.updateRecommendedFiles(recommendedFiles) + } + } + } + } + + private fun requestForDownload(file: OCFile, downloadBehaviour: String, packageName: String, activityName: String) { + val currentUser = user.orElseThrow(Supplier { RuntimeException() }) + if (!FileDownloadHelper.Companion.instance().isDownloading(currentUser, file)) { + FileDownloadHelper.Companion.instance().downloadFile( + currentUser, + file, + downloadBehaviour, + DownloadType.DOWNLOAD, + activityName, + packageName, + null + ) + } + } + + private fun sendDownloadedFile(packageName: String, activityName: String) { + val waitingToSend = mWaitingToSend + if (waitingToSend != null) { + val sendIntent = IntentUtil.createSendIntent(this, waitingToSend) + sendIntent.component = ComponentName(packageName, activityName) + + // Show dialog + val sendTitle = getString(R.string.activity_chooser_send_file_title) + startActivity(Intent.createChooser(sendIntent, sendTitle)) + } else { + Log_OC.e(TAG, "Trying to send a NULL OCFile") + } + + mWaitingToSend = null + } + + /** + * Requests the download of the received [OCFile] , updates the UI to monitor the download progress and + * prepares the activity to send the file when the download finishes. + * + * @param file [OCFile] to download and preview. + * @param packageName + * @param activityName + */ + fun startDownloadForSending(file: OCFile?, downloadBehaviour: String, packageName: String, activityName: String) { + mWaitingToSend = file + mWaitingToSend?.let { + requestForDownload(it, downloadBehaviour, packageName, activityName) + } + } + + fun startImagePreview(file: OCFile, showPreview: Boolean) { + val showDetailsIntent = Intent(this, PreviewImageActivity::class.java) + showDetailsIntent.putExtra(EXTRA_FILE, file) + showDetailsIntent.putExtra(EXTRA_LIVE_PHOTO_FILE, file.livePhotoVideo) + showDetailsIntent.putExtra( + EXTRA_USER, + user.orElseThrow(Supplier { RuntimeException() }) + ) + if (showPreview) { + startActivity(showDetailsIntent) + } else { + val fileOperationsHelper = + FileOperationsHelper(this, userAccountManager, connectivityService, editorUtils) + fileOperationsHelper.startSyncForFileAndIntent(file, showDetailsIntent) + } + } + + fun startImagePreview(file: OCFile, type: VirtualFolderType?, showPreview: Boolean) { + val showDetailsIntent = Intent(this, PreviewImageActivity::class.java) + showDetailsIntent.putExtra(EXTRA_FILE, file) + showDetailsIntent.putExtra(EXTRA_LIVE_PHOTO_FILE, file.livePhotoVideo) + showDetailsIntent.putExtra( + EXTRA_USER, + user.orElseThrow(Supplier { RuntimeException() }) + ) + showDetailsIntent.putExtra(PreviewImageActivity.EXTRA_VIRTUAL_TYPE, type) + + if (showPreview) { + startActivity(showDetailsIntent) + } else { + val fileOperationsHelper = FileOperationsHelper( + this, + userAccountManager, + connectivityService, + editorUtils + ) + fileOperationsHelper.startSyncForFileAndIntent(file, showDetailsIntent) + } + } + + /** + * Stars the preview of an already down media [OCFile]. + * + * @param file Media [OCFile] to preview. + * @param startPlaybackPosition Media position where the playback will be started, in milliseconds. + * @param autoplay When 'true', the playback will start without user interactions. + */ + fun startMediaPreview( + file: OCFile, + startPlaybackPosition: Long, + autoplay: Boolean, + showPreview: Boolean, + streamMedia: Boolean, + showInActivity: Boolean + ) { + val user = getUser() + if (!user.isPresent) { + return // not reachable under normal conditions + } + val actualUser = user.get() + if (showPreview && file.isDown && !file.isDownloading || streamMedia) { + if (showInActivity) { + startMediaActivity(file, startPlaybackPosition, autoplay, actualUser) + } else { + configureToolbarForPreview(file) + val mediaFragment: Fragment = newInstance(file, user.get(), startPlaybackPosition, autoplay, false) + setLeftFragment(mediaFragment, false) + } + } else { + val previewIntent = Intent() + previewIntent.putExtra(EXTRA_FILE, file) + previewIntent.putExtra(PreviewMediaFragment.EXTRA_START_POSITION, startPlaybackPosition) + previewIntent.putExtra(PreviewMediaFragment.EXTRA_AUTOPLAY, autoplay) + val fileOperationsHelper = + FileOperationsHelper(this, userAccountManager, connectivityService, editorUtils) + fileOperationsHelper.startSyncForFileAndIntent(file, previewIntent) + } + } + + private fun startMediaActivity(file: OCFile?, startPlaybackPosition: Long, autoplay: Boolean, user: User?) { + val previewMediaIntent = Intent(this, PreviewMediaActivity::class.java) + previewMediaIntent.putExtra(PreviewMediaActivity.EXTRA_FILE, file) + + // Safely handle the absence of a user + if (user != null) { + previewMediaIntent.putExtra(PreviewMediaActivity.EXTRA_USER, user) + } + + previewMediaIntent.putExtra(PreviewMediaActivity.EXTRA_START_POSITION, startPlaybackPosition) + previewMediaIntent.putExtra(PreviewMediaActivity.EXTRA_AUTOPLAY, autoplay) + startActivity(previewMediaIntent) + } + + fun configureToolbarForPreview(file: OCFile?) { + lockScrolling() + super.updateActionBarTitleAndHomeButton(file) + } + + /** + * Starts the preview of a text file [OCFile]. + * + * @param file Text [OCFile] to preview. + */ + fun startTextPreview(file: OCFile?, showPreview: Boolean) { + val optUser = user + if (!optUser.isPresent) { + // remnants of old unsafe system; do not crash, silently stop + return + } + val user = optUser.get() + if (showPreview) { + val fragment = PreviewTextFileFragment.create(user, file, searchOpen, searchQuery) + setLeftFragment(fragment, false) + configureToolbarForPreview(file) + } else { + val previewIntent = Intent() + previewIntent.putExtra(EXTRA_FILE, file) + previewIntent.putExtra(TEXT_PREVIEW, true) + val fileOperationsHelper = + FileOperationsHelper(this, userAccountManager, connectivityService, editorUtils) + fileOperationsHelper.startSyncForFileAndIntent(file, previewIntent) + } + } + + /** + * Starts rich workspace preview for a folder. + * + * @param folder [OCFile] to preview its rich workspace. + */ + fun startRichWorkspacePreview(folder: OCFile?) { + val args = Bundle() + args.putParcelable(EXTRA_FILE, folder) + configureToolbarForPreview(folder) + val textPreviewFragment = + Fragment.instantiate(applicationContext, PreviewTextStringFragment::class.java.name, args) + setLeftFragment(textPreviewFragment, false) + } + + fun startContactListFragment(file: OCFile?) { + val user = user.orElseThrow(Supplier { RuntimeException() }) + ContactsPreferenceActivity.startActivityWithContactsFile(this, user, file) + } + + fun startPdfPreview(file: OCFile) { + if (fileOperationsHelper.canOpenFile(file)) { + // prefer third party PDF apps + fileOperationsHelper.openFile(file) + } else { + val pdfFragment: Fragment = newInstance(file) + + setLeftFragment(pdfFragment, false) + configureToolbarForPreview(file) + setMainFabVisible(false) + } + } + + /** + * Requests the download of the received [OCFile] , updates the UI to monitor the download progress and + * prepares the activity to preview or open the file when the download finishes. + * + * @param file [OCFile] to download and preview. + * @param parentFolder [OCFile] containing above file + */ + fun startDownloadForPreview(file: OCFile, parentFolder: OCFile?) { + if (!file.isFileEligibleForImmediatePreview) { + val currentUser = user + if (currentUser.isPresent) { + val detailFragment: Fragment = FileDetailFragment.newInstance(file, parentFolder, currentUser.get()) + setLeftFragment(detailFragment, false) + } + } + + configureToolbarForPreview(file) + mWaitingToPreview = file + requestForDownload() + setFile(file) + } + + /** + * Opens EditImageActivity with given file loaded. If file is not available locally, it will be synced before + * opening the image editor. + * + * @param file [OCFile] (image) to be loaded into image editor + */ + fun startImageEditor(file: OCFile) { + if (file.isDown) { + val editImageIntent = Intent(this, EditImageActivity::class.java) + editImageIntent.putExtra(EditImageActivity.EXTRA_FILE, file) + startActivity(editImageIntent) + } else { + mWaitingToPreview = file + requestForDownload( + file, + EditImageActivity.OPEN_IMAGE_EDITOR, + packageName, + this.javaClass.simpleName + ) + updateActionBarTitleAndHomeButton(file) + setFile(file) + } + } + + /** + * Request stopping the upload/download operation in progress over the given [OCFile] file. + * + * @param file [OCFile] file which operation are wanted to be cancel + */ + fun cancelTransference(file: OCFile) { + fileOperationsHelper.cancelTransference(file) + if (mWaitingToPreview != null && mWaitingToPreview?.remotePath == file.remotePath) { + mWaitingToPreview = null + } + if (mWaitingToSend != null && mWaitingToSend?.remotePath == file.remotePath) { + mWaitingToSend = null + } + onTransferStateChanged(file, false, false) + } + + /** + * Request stopping all upload/download operations in progress over the given [OCFile] files. + * + * @param files collection of [OCFile] files which operations are wanted to be cancel + */ + fun cancelTransference(files: MutableCollection) { + for (file in files) { + cancelTransference(file) + } + } + + override fun onRefresh(ignoreETag: Boolean) { + syncAndUpdateFolder(ignoreETag, ignoreFocus = false) + } + + override fun onRefresh() { + syncAndUpdateFolder(ignoreETag = true, ignoreFocus = false) + } + + private fun syncAndUpdateFolder(ignoreETag: Boolean, ignoreFocus: Boolean) { + val listOfFiles = this.listOfFilesFragment + if (listOfFiles == null || listOfFiles.isSearchFragment) { + return + } + + val folder = listOfFiles.currentFile ?: return + startSyncFolderOperation(folder, ignoreETag, ignoreFocus) + } + + override fun showFiles(onDeviceOnly: Boolean, personalFiles: Boolean) { + super.showFiles(onDeviceOnly, personalFiles) + if (onDeviceOnly) { + updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_item_on_device)) + } + val ocFileListFragment = this.listOfFilesFragment + if (ocFileListFragment != null && + (ocFileListFragment !is GalleryFragment) && + (ocFileListFragment !is SharedListFragment) + ) { + ocFileListFragment.refreshDirectory() + } else { + this.leftFragment = OCFileListFragment() + } + } + + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onMessageEvent(event: SearchEvent) { + if (SearchRemoteOperation.SearchType.PHOTO_SEARCH == event.searchType) { + Log_OC.d(this, "Switch to photo search fragment") + this.leftFragment = GalleryFragment() + } else if (event.searchType == SearchRemoteOperation.SearchType.SHARED_FILTER) { + Log_OC.d(this, "Switch to Shared fragment") + this.leftFragment = SharedListFragment() + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(event: SyncEventFinished) { + val bundle = event.intent.extras + val file = bundle?.get(EXTRA_FILE) as OCFile? ?: return + + if (event.intent.getBooleanExtra(TEXT_PREVIEW, false)) { + startTextPreview(file, true) + } else if (bundle.containsKey(PreviewMediaFragment.EXTRA_START_POSITION)) { + val startPosition = bundle.get(PreviewMediaFragment.EXTRA_START_POSITION) as Long + val autoPlay = bundle.get(PreviewMediaFragment.EXTRA_AUTOPLAY) as Boolean + startMediaPreview( + file, + startPosition, + autoPlay, + true, + true, + true + ) + } else if (bundle.containsKey(PreviewImageActivity.EXTRA_VIRTUAL_TYPE)) { + val virtualType = bundle.get(PreviewImageActivity.EXTRA_VIRTUAL_TYPE) as VirtualFolderType? + startImagePreview( + file, + virtualType, + true + ) + } else { + startImagePreview(file, true) + } + } + + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onMessageEvent(event: TokenPushEvent?) { + if (!preferences.isKeysReInitEnabled()) { + PushUtils.reinitKeys(userAccountManager) + } else { + PushUtils.pushRegistrationToServer(userAccountManager, preferences.getPushToken()) + } + } + + public override fun onStart() { + super.onStart() + val optionalUser = user + val storageManager = getStorageManager() + if (optionalUser.isPresent && storageManager != null) { + /** Check whether the 'main' OCFile handled by the Activity is contained in the */ + // current Account + var file = getFile() + // get parent from path + if (file != null) { + if (file.isDown && file.lastSyncDateForProperties == 0L) { + // upload in progress - right now, files are not inserted in the local + // cache until the upload is successful get parent from path + val parentPath = + file.remotePath.substring(0, file.remotePath.lastIndexOf(file.fileName)) + if (storageManager.getFileByPath(parentPath) == null) { + file = null // not able to know the directory where the file is uploading + } + } else { + file = storageManager.getFileByPath(file.remotePath) + // currentDir = null if not in the current Account + } + } + if (file == null) { + // fall back to root folder + file = storageManager.getFileByPath(OCFile.ROOT_PATH) // never returns null + } + setFile(file) + + val user = optionalUser.get() + setupDrawer() + + mSwitchAccountButton.tag = user.accountName + DisplayUtils.setAvatar( + user, + this, + getResources().getDimension(R.dimen.nav_drawer_menu_avatar_radius), + getResources(), + mSwitchAccountButton, + this + ) + val userChanged = (user.accountName != lastDisplayedAccountName) + if (userChanged) { + Log_OC.d(TAG, "Initializing Fragments in onAccountChanged..") + initFragments() + if (file.isFolder && TextUtils.isEmpty(searchQuery)) { + startSyncFolderOperation(file, false) + } + } else { + updateActionBarTitleAndHomeButton(if (file.isFolder) null else file) + } + } + + val newLastDisplayedAccountName = optionalUser.orElse(null).accountName + preferences.lastDisplayedAccountName = newLastDisplayedAccountName + lastDisplayedAccountName = newLastDisplayedAccountName + + EventBus.getDefault().post(TokenPushEvent()) + checkForNewDevVersionNecessary(applicationContext) + } + + override fun onRestart() { + super.onRestart() + checkForNewDevVersionNecessary(applicationContext) + } + + fun setSearchQuery(query: String?) { + searchQuery = query + } + + private fun handleOpenFileViaIntent(intent: Intent) { + DisplayUtils.showSnackMessage(this, getString(R.string.retrieving_file)) + + val userName = intent.getStringExtra(KEY_ACCOUNT) + val fileId = intent.getStringExtra(KEY_FILE_ID) + val filePath = intent.getStringExtra(KEY_FILE_PATH) + + val intentData = intent.data + if (userName == null && fileId == null && intentData != null) { + openDeepLink(intentData) + } else { + val optionalUser = if (userName == null) user else userAccountManager.getUser(userName) + if (optionalUser.isPresent) { + if (!TextUtils.isEmpty(fileId)) { + openFile(optionalUser.get(), fileId) + } else if (!TextUtils.isEmpty(filePath)) { + openFileByPath(optionalUser.get(), filePath) + } else { + accountClicked(optionalUser.get().hashCode()) + } + } else { + DisplayUtils.showSnackMessage(this, getString(R.string.associated_account_not_found)) + } + } + } + + private fun openDeepLink(uri: Uri) { + val linkHandler = DeepLinkHandler(userAccountManager) + val match = linkHandler.parseDeepLink(uri) + + if (match == null) { + handleDeepLink(uri) + } else if (match.users.isEmpty()) { + DisplayUtils.showSnackMessage(this, getString(R.string.associated_account_not_found)) + } else if (match.users.size == SINGLE_USER_SIZE) { + openFile(match.users[0], match.fileId) + } else { + selectUserAndOpenFile(match.users.toMutableList(), match.fileId) + } + } + + private fun selectUserAndOpenFile(users: MutableList, fileId: String?) { + val userNames = arrayOfNulls(users.size) + for (i in userNames.indices) { + userNames[i] = users[i]?.accountName + } + val builder = MaterialAlertDialogBuilder(this) + builder.setTitle(R.string.common_choose_account) + .setItems(userNames) { dialog: DialogInterface?, which: Int -> + val user = users[which] + openFile(user, fileId) + showLoadingDialog(getString(R.string.retrieving_file)) + } + + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(applicationContext, builder) + + val dialog = builder.create() + dismissLoadingDialog() + dialog.show() + } + + private fun openFile(user: User?, fileId: String?) { + setUser(user) + + if (fileId == null) { + onFileRequestError(null) + return + } + + var storageManager = getStorageManager() + + if (storageManager == null) { + storageManager = FileDataStorageManager(user, contentResolver) + } + + val fetchRemoteFileTask = FetchRemoteFileTask(user, fileId, storageManager, this) + fetchRemoteFileTask.execute() + } + + private fun openFileByPath(user: User, filepath: String?) { + setUser(user) + + if (filepath == null) { + onFileRequestError(null) + return + } + + var storageManager = getStorageManager() + + if (storageManager == null) { + storageManager = FileDataStorageManager(user, contentResolver) + } + + val client: OwnCloudClient + try { + client = clientFactory.create(user) + } catch (_: CreationException) { + onFileRequestError(null) + return + } + + val getRemoteFileTask = GetRemoteFileTask(this, filepath, client, storageManager, user) + asyncRunner.postQuickTask( + getRemoteFileTask, + { result: GetRemoteFileTask.Result -> this.onFileRequestResult(result) }, + { throwable: Throwable? -> this.onFileRequestError(throwable) } + ) + } + + private fun onFileRequestError(throwable: Throwable?) { + dismissLoadingDialog() + DisplayUtils.showSnackMessage(this, getString(R.string.error_retrieving_file)) + Log_OC.e(TAG, "Requesting file from remote failed!", throwable) + } + + private fun onFileRequestResult(result: GetRemoteFileTask.Result) { + dismissLoadingDialog() + + file = result.file + + val fileFragment = OCFileListFragment() + this.leftFragment = fileFragment + + supportFragmentManager.executePendingTransactions() + + fileFragment.onItemClicked(result.file) + } + + fun performUnifiedSearch(query: String, listOfHiddenFiles: ArrayList?) { + val path = currentDir?.decryptedRemotePath + ?: run { + Log_OC.w(TAG, "currentDir is null, using ROOT_PATH") + OCFile.ROOT_PATH + } + + val unifiedSearchFragment = + UnifiedSearchFragment.Companion.newInstance( + query, + listOfHiddenFiles, + path + ) + setLeftFragment(unifiedSearchFragment, false) + } + + fun setMainFabVisible(visible: Boolean) { + val visibility = if (visible) View.VISIBLE else View.GONE + binding.fabMain.visibility = visibility + } + + fun showFile(selectedFile: OCFile?, message: String?) { + dismissLoadingDialog() + + getOCFileListFragmentFromFile(object : TransactionInterface { + override fun onOCFileListFragmentComplete(listOfFiles: OCFileListFragment) { + if (TextUtils.isEmpty(message)) { + val temp = file + file = getCurrentDir() + listOfFiles.listDirectory(getCurrentDir(), temp, MainApp.isOnlyOnDevice(), false) + updateActionBarTitleAndHomeButton(null) + } else { + val view = listOfFiles.view + if (view != null) { + DisplayUtils.showSnackMessage(view, message) + } + } + if (selectedFile != null) { + listOfFiles.onItemClicked(selectedFile) + } + } + }) + } + + // region MetadataSyncJob + private fun startMetadataSyncForRoot() { + backgroundJobManager.startMetadataSyncJob(OCFile.ROOT_PATH) + } + + private fun startMetadataSyncForCurrentDir() { + val currentDirId = file?.decryptedRemotePath ?: return + backgroundJobManager.startMetadataSyncJob(currentDirId) + } + // endregion + + companion object { + const val RESTART: String = "RESTART" + const val ALL_FILES: String = "ALL_FILES" + const val LIST_GROUPFOLDERS: String = "LIST_GROUPFOLDERS" + const val SINGLE_USER_SIZE: Int = 1 + const val OPEN_FILE: String = "NC_OPEN_FILE" + + const val TAG_PUBLIC_LINK: String = "PUBLIC_LINK" + const val FTAG_CHOOSER_DIALOG: String = "CHOOSER_DIALOG" + const val KEY_FILE_ID: String = "KEY_FILE_ID" + const val KEY_FILE_PATH: String = "KEY_FILE_PATH" + const val KEY_ACCOUNT: String = "KEY_ACCOUNT" + const val KEY_IS_SORT_GROUP_VISIBLE: String = "KEY_IS_SORT_GROUP_VISIBLE" + + private const val KEY_WAITING_TO_PREVIEW = "WAITING_TO_PREVIEW" + private const val KEY_SYNC_IN_PROGRESS = "SYNC_IN_PROGRESS" + private const val KEY_WAITING_TO_SEND = "WAITING_TO_SEND" + private const val DIALOG_TAG_SHOW_TOS = "DIALOG_TAG_SHOW_TOS" + + private const val ON_RESUMED_RESET_DELAY = 10000L + + const val ACTION_DETAILS: String = "com.owncloud.android.ui.activity.action.DETAILS" + + @JvmField + val REQUEST_CODE__SELECT_CONTENT_FROM_APPS: Int = REQUEST_CODE__LAST_SHARED + 1 + + @JvmField + val REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM: Int = REQUEST_CODE__LAST_SHARED + 2 + + @JvmField + val REQUEST_CODE__MOVE_OR_COPY_FILES: Int = REQUEST_CODE__LAST_SHARED + 3 + + @JvmField + val REQUEST_CODE__UPLOAD_FROM_CAMERA: Int = REQUEST_CODE__LAST_SHARED + 5 + + @JvmField + val REQUEST_CODE__UPLOAD_FROM_VIDEO_CAMERA: Int = REQUEST_CODE__LAST_SHARED + 6 + + protected val DELAY_TO_REQUEST_REFRESH_OPERATION_LATER: Long = DELAY_TO_REQUEST_OPERATIONS_LATER + 350 + + private val TAG: String = FileDisplayActivity::class.java.getSimpleName() + + const val TAG_LIST_OF_FILES: String = "LIST_OF_FILES" + + const val TEXT_PREVIEW: String = "TEXT_PREVIEW" + + const val KEY_IS_SEARCH_OPEN: String = "IS_SEARCH_OPEN" + const val KEY_SEARCH_QUERY: String = "SEARCH_QUERY" + + @JvmStatic + fun openFileIntent(context: Context?, user: User?, file: OCFile?): Intent { + val intent = Intent(context, PreviewImageActivity::class.java) + intent.putExtra(EXTRA_FILE, file) + intent.putExtra(EXTRA_USER, user) + return intent + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FilePickerActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FilePickerActivity.kt index 1814412..050331b 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FilePickerActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FilePickerActivity.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2018 Tobias Kaminsky * SPDX-FileCopyrightText: 2018 Nextcloud GmbH - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activity diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt index 0961de2..7599a5b 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt @@ -3,7 +3,7 @@ * * SPDX-FileCopyrightText: 2023 TSI-mc * SPDX-FileCopyrightText: 2022 Álvaro Brey - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activity @@ -21,8 +21,11 @@ import android.view.Menu import android.view.MenuItem import android.view.View import androidx.activity.OnBackPressedCallback +import androidx.lifecycle.lifecycleScope import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.nextcloud.client.account.User import com.nextcloud.client.di.Injectable +import com.nextcloud.utils.fileNameValidator.FileNameValidator import com.owncloud.android.R import com.owncloud.android.databinding.FilesFolderPickerBinding import com.owncloud.android.databinding.FilesPickerBinding @@ -39,6 +42,7 @@ import com.owncloud.android.syncadapter.FileSyncAdapter import com.owncloud.android.ui.dialog.CreateFolderDialogFragment import com.owncloud.android.ui.dialog.SortingOrderDialogFragment.OnSortingOrderListener import com.owncloud.android.ui.events.SearchEvent +import com.owncloud.android.ui.fragment.EmptyListState import com.owncloud.android.ui.fragment.FileFragment import com.owncloud.android.ui.fragment.OCFileListFragment import com.owncloud.android.utils.DataHolderUtil @@ -46,6 +50,8 @@ import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.ErrorMessageAdapter import com.owncloud.android.utils.FileSortOrder import com.owncloud.android.utils.PathUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import java.io.File import javax.inject.Inject @@ -58,7 +64,6 @@ open class FolderPickerActivity : OnSortingOrderListener { private var mSyncBroadcastReceiver: SyncBroadcastReceiver? = null - private var mSyncInProgress = false private var mSearchOnlyFolders = false var isDoNotEnterEncryptedFolder = false private set @@ -82,6 +87,8 @@ open class FolderPickerActivity : folderPickerBinding = FilesFolderPickerBinding.inflate(layoutInflater) setContentView(folderPickerBinding.root) } + + OCFileListFragment.isMultipleFileSelectedForCopyOrMove = true } override fun onCreate(savedInstanceState: Bundle?) { @@ -101,8 +108,12 @@ open class FolderPickerActivity : } updateActionBarTitleAndHomeButtonByString(captionText) - setBackgroundText() - handleOnBackPressed() + handleBackPress() + } + + override fun onDestroy() { + OCFileListFragment.isMultipleFileSelectedForCopyOrMove = false + super.onDestroy() } private fun setupActionBar() { @@ -142,7 +153,7 @@ open class FolderPickerActivity : } } - private fun handleOnBackPressed() { + private fun handleBackPress() { onBackPressedDispatcher.addCallback( this, object : OnBackPressedCallback(true) { @@ -202,30 +213,6 @@ open class FolderPickerActivity : transaction.commit() } - /** - * Show a text message on screen view for notifying user if content is loading or folder is empty - */ - private fun setBackgroundText() { - val listFragment = listOfFilesFragment - - if (listFragment == null) { - Log_OC.e(TAG, "OCFileListFragment is null") - } - - listFragment?.let { - if (!mSyncInProgress) { - it.setMessageForEmptyList( - R.string.folder_list_empty_headline, - R.string.file_list_empty_moving, - R.drawable.ic_list_empty_create_folder, - true - ) - } else { - it.setEmptyListLoadingMessage() - } - } - } - protected val listOfFilesFragment: OCFileListFragment? get() { val listOfFiles = supportFragmentManager.findFragmentByTag(TAG_LIST_OF_FOLDERS) @@ -255,30 +242,39 @@ open class FolderPickerActivity : } private fun startSyncFolderOperation(folder: OCFile?, ignoreETag: Boolean) { - val currentSyncTime = System.currentTimeMillis() - mSyncInProgress = true - - RefreshFolderOperation( - folder, - currentSyncTime, - false, - ignoreETag, - storageManager, - user.orElseThrow { RuntimeException("User not set") }, - applicationContext - ).also { - it.execute(account, this, null, null) + val optionalUser = user ?: return + if (optionalUser.isEmpty) { + return } + val user: User = optionalUser.get() + listOfFilesFragment?.setEmptyListMessage(EmptyListState.LOADING) - listOfFilesFragment?.isLoading = true - setBackgroundText() + lifecycleScope.launch(Dispatchers.IO) { + val currentSyncTime = System.currentTimeMillis() + val operation = RefreshFolderOperation( + folder, + currentSyncTime, + false, + ignoreETag, + storageManager, + user, + applicationContext + ) + operation.execute( + account, + this@FolderPickerActivity, + { _, _ -> + listOfFilesFragment?.setEmptyListMessage(EmptyListState.LOCAL_FILE_LIST_EMPTY_FILE) + }, + null + ) + } } override fun onResume() { super.onResume() Log_OC.e(TAG, "onResume() start") - listOfFilesFragment?.isLoading = mSyncInProgress refreshListOfFilesFragment(false) file = listOfFilesFragment?.currentFile updateUiElements() @@ -292,13 +288,11 @@ open class FolderPickerActivity : Log_OC.d(TAG, "onResume() end") } - private fun getSyncIntentFilter(): IntentFilter { - return IntentFilter(FileSyncAdapter.EVENT_FULL_SYNC_START).apply { - addAction(FileSyncAdapter.EVENT_FULL_SYNC_END) - addAction(FileSyncAdapter.EVENT_FULL_SYNC_FOLDER_CONTENTS_SYNCED) - addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED) - addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED) - } + private fun getSyncIntentFilter(): IntentFilter = IntentFilter(FileSyncAdapter.EVENT_FULL_SYNC_START).apply { + addAction(FileSyncAdapter.EVENT_FULL_SYNC_END) + addAction(FileSyncAdapter.EVENT_FULL_SYNC_FOLDER_CONTENTS_SYNCED) + addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED) + addAction(RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED) } override fun onPause() { @@ -328,7 +322,7 @@ open class FolderPickerActivity : } else if (itemId == android.R.id.home) { val currentDir = currentFolder if (currentDir != null && currentDir.parentId != 0L) { - onBackPressed() + onBackPressedDispatcher.onBackPressed() } } else { retval = super.onOptionsItemSelected(item) @@ -379,26 +373,47 @@ open class FolderPickerActivity : private fun toggleChooseEnabled() { if (this is FilePickerActivity) { return + } + + val selectedFolderPathTitle = getSelectedFolderPathTitle() + val isFolderPathValid = if (selectedFolderPathTitle != null) { + FileNameValidator.checkFolderPath(selectedFolderPathTitle, capabilities, this) } else { - folderPickerBinding.folderPickerBtnCopy.isEnabled = checkFolderSelectable() - folderPickerBinding.folderPickerBtnMove.isEnabled = checkFolderSelectable() + true + } + + checkButtonStates(isFolderPathValid) + + if (!isFolderPathValid) { + DisplayUtils.showSnackMessage( + this, + R.string.file_name_validator_error_contains_reserved_names_or_invalid_characters + ) + return + } + } + + private fun checkButtonStates(isConditionMet: Boolean) { + folderPickerBinding.run { + folderPickerBtnChoose.isEnabled = isConditionMet + folderPickerBtnCopy.isEnabled = isFolderSelectable(COPY) && isConditionMet + folderPickerBtnMove.isEnabled = isFolderSelectable(MOVE) && isConditionMet } } // for copy and move, disable selecting parent folder of target files - private fun checkFolderSelectable(): Boolean { - return when { - action != MOVE_OR_COPY -> true - targetFilePaths.isNullOrEmpty() -> true - file?.isFolder != true -> true + private fun isFolderSelectable(type: String): Boolean = when { + action != MOVE_OR_COPY -> true + action == MOVE_OR_COPY && type == COPY -> true + targetFilePaths.isNullOrEmpty() -> true + file?.isFolder != true -> true - // all of the target files are already in the selected directory - targetFilePaths?.all { PathUtils.isDirectParent(file.remotePath, it) } == true -> false + // all of the target files are already in the selected directory + targetFilePaths?.all { PathUtils.isDirectParent(file?.remotePath ?: "", it) } == true -> false - // some of the target files are parents of the selected folder - targetFilePaths?.any { PathUtils.isAncestor(it, file.remotePath) } == true -> false - else -> true - } + // some of the target files are parents of the selected folder + targetFilePaths?.any { PathUtils.isAncestor(it, file?.remotePath ?: "") } == true -> false + else -> true } private fun updateNavigationElementsInActionBar() { @@ -407,13 +422,17 @@ open class FolderPickerActivity : val atRoot = (currentDir == null || currentDir.parentId == 0L) actionBar.setDisplayHomeAsUpEnabled(!atRoot) actionBar.setHomeButtonEnabled(!atRoot) - val title = if (atRoot) captionText ?: "" else currentDir?.fileName - title?.let { - viewThemeUtils.files.themeActionBar(this, actionBar, title) + getSelectedFolderPathTitle()?.let { + viewThemeUtils.files.themeActionBar(this, actionBar, it) } } } + private fun getSelectedFolderPathTitle(): String? { + val atRoot = (currentDir == null || currentDir?.parentId == 0L) + return if (atRoot) captionText ?: "" else currentDir?.fileName + } + private fun initControls() { if (this is FilePickerActivity) { viewThemeUtils.material.colorMaterialButtonPrimaryFilled(filesPickerBinding.folderPickerBtnCancel) @@ -441,6 +460,7 @@ open class FolderPickerActivity : } } + @Suppress("MagicNumber") private fun processOperation(action: String?) { val i = intent val resultData = Intent() @@ -475,10 +495,7 @@ open class FolderPickerActivity : * @param operation Creation operation performed. * @param result Result of the creation. */ - private fun onCreateFolderOperationFinish( - operation: CreateFolderOperation, - result: RemoteOperationResult<*> - ) { + private fun onCreateFolderOperationFinish(operation: CreateFolderOperation, result: RemoteOperationResult<*>) { if (result.isSuccess) { val fileListFragment = listOfFilesFragment fileListFragment?.onItemClicked(storageManager.getFileByPath(operation.remotePath)) @@ -532,15 +549,13 @@ open class FolderPickerActivity : return } - if (FileSyncAdapter.EVENT_FULL_SYNC_START == event) { - mSyncInProgress = true - } else { + if (FileSyncAdapter.EVENT_FULL_SYNC_START != event) { var (currentFile, currentDir) = getCurrentFileAndDirectory() if (currentDir == null) { browseRootForRemovedFolder() } else { - if (currentFile == null && !file.isFolder) { + if (currentFile == null && file?.isFolder == false) { // currently selected file was removed in the server, and now we know it currentFile = currentDir } @@ -550,29 +565,22 @@ open class FolderPickerActivity : file = currentFile } - mSyncInProgress = ( - FileSyncAdapter.EVENT_FULL_SYNC_END != event && - RefreshFolderOperation.EVENT_SINGLE_FOLDER_SHARES_SYNCED != event - ) - - checkCredentials(syncResult, context, event) + checkCredentials(syncResult, event) } DataHolderUtil.getInstance().delete(intent.getStringExtra(FileSyncAdapter.EXTRA_RESULT)) - Log_OC.d(TAG, "Setting progress visibility to $mSyncInProgress") - listOfFilesFragment?.isLoading = mSyncInProgress - setBackgroundText() } catch (e: RuntimeException) { Log_OC.e(TAG, "Error on broadcast receiver", e) // avoid app crashes after changing the serial id of RemoteOperationResult // in owncloud library with broadcast notifications pending to process DataHolderUtil.getInstance().delete(intent.getStringExtra(FileSyncAdapter.EXTRA_RESULT)) + } finally { + listOfFilesFragment?.setEmptyListMessage(EmptyListState.LOCAL_FILE_LIST_EMPTY_FILE) } } private fun getCurrentFileAndDirectory(): Pair { - val currentFile = - if (file == null) null else storageManager.getFileByEncryptedRemotePath(file.remotePath) + val currentFile = file?.let { storageManager.getFileByEncryptedRemotePath(it.remotePath) } val currentDir = if (currentFolder == null) { null @@ -594,15 +602,16 @@ open class FolderPickerActivity : browseToRoot() } - private fun checkCredentials(syncResult: RemoteOperationResult<*>, context: Context, event: String?) { + private fun checkCredentials(syncResult: RemoteOperationResult<*>, event: String?) { if (RefreshFolderOperation.EVENT_SINGLE_FOLDER_CONTENTS_SYNCED == event && !syncResult.isSuccess ) { - if (ResultCode.UNAUTHORIZED == syncResult.code || ( + if (ResultCode.UNAUTHORIZED == syncResult.code || + ( syncResult.isException && syncResult.exception is AuthenticatorException ) ) { - requestCredentialsUpdate(context) + requestCredentialsUpdate() } else if (ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED == syncResult.code) { showUntrustedCertDialog(syncResult) } @@ -663,6 +672,8 @@ open class FolderPickerActivity : const val MOVE_OR_COPY = "MOVE_OR_COPY" const val CHOOSE_LOCATION = "CHOOSE_LOCATION" private val TAG = FolderPickerActivity::class.java.simpleName + private const val MOVE = "MOVE" + private const val COPY = "COPY" const val TAG_LIST_OF_FOLDERS = "LIST_OF_FOLDERS" } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/InternalTwoWaySyncActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/InternalTwoWaySyncActivity.kt new file mode 100644 index 0000000..89d37b8 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/InternalTwoWaySyncActivity.kt @@ -0,0 +1,236 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Tobias Kaminsky + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.activity + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.ArrayAdapter +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.client.di.Injectable +import com.nextcloud.client.jobs.BackgroundJobManager +import com.nextcloud.client.jobs.download.FileDownloadWorker +import com.nextcloud.utils.extensions.hourPlural +import com.nextcloud.utils.extensions.minPlural +import com.nextcloud.utils.extensions.setVisibleIf +import com.owncloud.android.R +import com.owncloud.android.databinding.InternalTwoWaySyncLayoutBinding +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.adapter.InternalTwoWaySyncAdapter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes + +class InternalTwoWaySyncActivity : + DrawerActivity(), + Injectable, + InternalTwoWaySyncAdapter.InternalTwoWaySyncAdapterOnUpdate { + private val tag = "InternalTwoWaySyncActivity" + + @Inject + lateinit var backgroundJobManager: BackgroundJobManager + + lateinit var binding: InternalTwoWaySyncLayoutBinding + + private lateinit var internalTwoWaySyncAdapter: InternalTwoWaySyncAdapter + private var disableForAllFoldersMenuButton: MenuItem? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + internalTwoWaySyncAdapter = InternalTwoWaySyncAdapter(fileDataStorageManager, user.get(), this, this) + + binding = InternalTwoWaySyncLayoutBinding.inflate(layoutInflater) + setContentView(binding.root) + + setupToolbar() + setupActionBar() + setupTwoWaySyncAdapter() + setupEmptyList() + setupTwoWaySyncToggle() + setupTwoWaySyncInterval() + checkLayoutVisibilities(preferences.isTwoWaySyncEnabled) + } + + private fun setupActionBar() { + updateActionBarTitleAndHomeButtonByString(getString(R.string.two_way_sync_activity_title)) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + @SuppressLint("NotifyDataSetChanged") + private fun setupTwoWaySyncAdapter() { + if (preferences.isTwoWaySyncEnabled) { + binding.run { + list.run { + setEmptyView(emptyList.emptyListView) + adapter = internalTwoWaySyncAdapter + layoutManager = LinearLayoutManager(this@InternalTwoWaySyncActivity) + adapter?.notifyDataSetChanged() + } + } + } + } + + private fun setupEmptyList() { + binding.emptyList.run { + emptyListViewHeadline.run { + visibility = View.VISIBLE + setText(R.string.two_way_sync_activity_empty_list_title) + } + + emptyListViewText.run { + visibility = View.VISIBLE + setText(R.string.two_way_sync_activity_empty_list_desc) + } + + emptyListIcon.run { + visibility = View.VISIBLE + setImageDrawable( + viewThemeUtils.platform.tintDrawable( + context, + R.drawable.ic_sync, + ColorRole.PRIMARY + ) + ) + } + } + } + + @Suppress("TooGenericExceptionCaught") + private fun disableTwoWaySyncAndWorkers() { + lifecycleScope.launch(Dispatchers.IO) { + try { + backgroundJobManager.cancelTwoWaySyncJob() + + val currentUser = user.get() + + val folders = fileDataStorageManager.getInternalTwoWaySyncFolders(currentUser) + folders.forEach { folder -> + FileDownloadWorker.cancelOperation(currentUser.accountName, folder.fileId) + backgroundJobManager.cancelFilesDownloadJob(currentUser, folder.fileId) + + folder.internalFolderSyncTimestamp = -1L + fileDataStorageManager.saveFile(folder) + } + + withContext(Dispatchers.Main) { + internalTwoWaySyncAdapter.update() + } + } catch (e: Exception) { + Log_OC.d(tag, "Error caught at disableTwoWaySyncAndWorkers: $e") + } + } + } + + @Suppress("MagicNumber") + private fun setupTwoWaySyncInterval() { + val durations = listOf( + 15.minutes to minPlural(15), + 30.minutes to minPlural(30), + 45.minutes to minPlural(45), + 1.hours to hourPlural(1), + 2.hours to hourPlural(2), + 4.hours to hourPlural(4), + 6.hours to hourPlural(6), + 8.hours to hourPlural(8), + 12.hours to hourPlural(12), + 24.hours to hourPlural(24) + ) + val selectedDuration = durations.find { it.first.inWholeMinutes == preferences.twoWaySyncInterval } + + val adapter = ArrayAdapter( + this, + android.R.layout.simple_dropdown_item_1line, + durations.map { it.second } + ) + + binding.twoWaySyncInterval.run { + setAdapter(adapter) + setText(selectedDuration?.second ?: minPlural(15), false) + setOnItemClickListener { _, _, position, _ -> + handleDurationSelected(durations[position].first.inWholeMinutes) + } + } + } + + private fun handleDurationSelected(duration: Long) { + preferences.twoWaySyncInterval = duration + backgroundJobManager.scheduleInternal2WaySync(duration) + } + + private fun setupTwoWaySyncToggle() { + binding.twoWaySyncToggle.isChecked = preferences.isTwoWaySyncEnabled + binding.twoWaySyncToggle.setOnCheckedChangeListener { _, isChecked -> + preferences.setTwoWaySyncStatus(isChecked) + setupTwoWaySyncAdapter() + checkLayoutVisibilities(isChecked) + checkDisableForAllFoldersMenuButtonVisibility() + + if (isChecked) { + backgroundJobManager.scheduleInternal2WaySync(preferences.twoWaySyncInterval) + } else { + backgroundJobManager.cancelTwoWaySyncJob() + } + } + } + + private fun checkLayoutVisibilities(condition: Boolean) { + binding.listFrameLayout.setVisibleIf(condition) + binding.twoWaySyncIntervalLayout.setVisibleIf(condition) + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.activity_internal_two_way_sync, menu) + disableForAllFoldersMenuButton = menu?.findItem(R.id.action_dismiss_two_way_sync) + checkDisableForAllFoldersMenuButtonVisibility() + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressedDispatcher.onBackPressed() + } + R.id.action_dismiss_two_way_sync -> { + disableTwoWaySyncAndWorkers() + } + } + + return super.onOptionsItemSelected(item) + } + + private fun checkDisableForAllFoldersMenuButtonVisibility() { + lifecycleScope.launch { + val folderSize = withContext(Dispatchers.IO) { + fileDataStorageManager.getInternalTwoWaySyncFolders(user.get()).size + } + + checkDisableForAllFoldersMenuButtonVisibility(preferences.isTwoWaySyncEnabled, folderSize) + } + } + + private fun checkDisableForAllFoldersMenuButtonVisibility(isTwoWaySyncEnabled: Boolean, folderSize: Int) { + val showDisableButton = isTwoWaySyncEnabled && folderSize > 0 + + disableForAllFoldersMenuButton?.let { + it.setVisible(showDisableButton) + it.setEnabled(showDisableButton) + } + } + + override fun onUpdate(folderSize: Int) { + checkDisableForAllFoldersMenuButtonVisibility(preferences.isTwoWaySyncEnabled, folderSize) + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java deleted file mode 100644 index 03492fa..0000000 --- a/app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java +++ /dev/null @@ -1,475 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2024 Alper Ozturk - * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-FileCopyrightText: 2020 Chawki Chouib - * SPDX-FileCopyrightText: 2019 Tobias Kaminsky - * SPDX-FileCopyrightText: 2016-2018 Andy Scherzinger - * SPDX-License-Identifier: GPL-2.0-only AND AGPL-3.0-or-later - */ -package com.owncloud.android.ui.activity; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AccountManagerCallback; -import android.accounts.AccountManagerFuture; -import android.accounts.OperationCanceledException; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.Bundle; -import android.os.Handler; -import android.view.MenuItem; -import android.view.View; - -import com.google.common.collect.Sets; -import com.nextcloud.client.account.User; -import com.nextcloud.client.account.UserAccountManager; -import com.nextcloud.client.jobs.BackgroundJobManager; -import com.nextcloud.client.jobs.download.FileDownloadHelper; -import com.nextcloud.client.onboarding.FirstRunActivity; -import com.nextcloud.model.WorkerState; -import com.nextcloud.model.WorkerStateLiveData; -import com.nextcloud.utils.extensions.BundleExtensionsKt; -import com.owncloud.android.MainApp; -import com.owncloud.android.R; -import com.owncloud.android.authentication.AuthenticatorActivity; -import com.owncloud.android.datamodel.ArbitraryDataProvider; -import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.lib.common.OwnCloudAccount; -import com.owncloud.android.lib.common.UserInfo; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.operations.DownloadFileOperation; -import com.owncloud.android.services.OperationsService; -import com.owncloud.android.ui.adapter.UserListAdapter; -import com.owncloud.android.ui.adapter.UserListItem; -import com.owncloud.android.ui.dialog.AccountRemovalDialog; -import com.owncloud.android.ui.events.AccountRemovedEvent; -import com.owncloud.android.ui.helpers.FileOperationsHelper; - -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import javax.inject.Inject; - -import androidx.annotation.VisibleForTesting; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.widget.PopupMenu; -import androidx.fragment.app.FragmentManager; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import static com.owncloud.android.ui.activity.UserInfoActivity.KEY_USER_DATA; -import static com.owncloud.android.ui.adapter.UserListAdapter.KEY_DISPLAY_NAME; -import static com.owncloud.android.ui.adapter.UserListAdapter.KEY_USER_INFO_REQUEST_CODE; - -/** - * An Activity that allows the user to manage accounts. - */ -public class ManageAccountsActivity extends FileActivity implements UserListAdapter.Listener, - AccountManagerCallback, - ComponentsGetter, - UserListAdapter.ClickListener { - private static final String TAG = ManageAccountsActivity.class.getSimpleName(); - - public static final String KEY_ACCOUNT_LIST_CHANGED = "ACCOUNT_LIST_CHANGED"; - public static final String KEY_CURRENT_ACCOUNT_CHANGED = "CURRENT_ACCOUNT_CHANGED"; - public static final String PENDING_FOR_REMOVAL = UserAccountManager.PENDING_FOR_REMOVAL; - - private static final int KEY_DELETE_CODE = 101; - private static final int SINGLE_ACCOUNT = 1; - private static final int MIN_MULTI_ACCOUNT_SIZE = 2; - - private RecyclerView recyclerView; - private final Handler handler = new Handler(); - private String accountName; - private UserListAdapter userListAdapter; - private Set originalUsers; - private String originalCurrentUser; - - private ArbitraryDataProvider arbitraryDataProvider; - private boolean multipleAccountsSupported; - - private String workerAccountName; - private DownloadFileOperation workerCurrentDownload; - - @Inject BackgroundJobManager backgroundJobManager; - @Inject UserAccountManager accountManager; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.accounts_layout); - - recyclerView = findViewById(R.id.account_list); - - setupToolbar(); - - // set the back button from action bar - ActionBar actionBar = getSupportActionBar(); - - // check if is not null - if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setDisplayShowHomeEnabled(true); - viewThemeUtils.files.themeActionBar(this, actionBar, R.string.prefs_manage_accounts); - } - - List users = accountManager.getAllUsers(); - originalUsers = toAccountNames(users); - - Optional currentUser = getUser(); - if (currentUser.isPresent()) { - originalCurrentUser = currentUser.get().getAccountName(); - } - - arbitraryDataProvider = new ArbitraryDataProviderImpl(this); - - multipleAccountsSupported = getResources().getBoolean(R.bool.multiaccount_support); - - userListAdapter = new UserListAdapter(this, - accountManager, - getUserListItems(), - this, - multipleAccountsSupported, - true, - true, - viewThemeUtils); - - recyclerView.setAdapter(userListAdapter); - recyclerView.setLayoutManager(new LinearLayoutManager(this)); - observeWorkerState(); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (resultCode == KEY_DELETE_CODE && data != null) { - Bundle bundle = data.getExtras(); - if (bundle != null && bundle.containsKey(UserInfoActivity.KEY_ACCOUNT)) { - final Account account = BundleExtensionsKt.getParcelableArgument(bundle, UserInfoActivity.KEY_ACCOUNT, Account.class); - if (account != null) { - User user = accountManager.getUser(account.name).orElseThrow(RuntimeException::new); - accountName = account.name; - performAccountRemoval(user); - } - } - } - } - - @Override - public void onBackPressed() { - Intent resultIntent = new Intent(); - if (accountManager.getAllUsers().size() > 0) { - resultIntent.putExtra(KEY_ACCOUNT_LIST_CHANGED, hasAccountListChanged()); - resultIntent.putExtra(KEY_CURRENT_ACCOUNT_CHANGED, hasCurrentAccountChanged()); - setResult(RESULT_OK, resultIntent); - - super.onBackPressed(); - } else { - final Intent intent = new Intent(this, AuthenticatorActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(intent); - finish(); - } - } - - /** - * checks the set of actual accounts against the set of original accounts when the activity has been started. - * - * @return true if account list has changed, false if not - */ - private boolean hasAccountListChanged() { - List users = accountManager.getAllUsers(); - List newList = new ArrayList<>(); - for (User user : users) { - boolean pendingForRemoval = arbitraryDataProvider.getBooleanValue(user, PENDING_FOR_REMOVAL); - - if (!pendingForRemoval) { - newList.add(user); - } - } - Set actualAccounts = toAccountNames(newList); - return !originalUsers.equals(actualAccounts); - } - - private static Set toAccountNames(Collection users) { - Set accountNames = Sets.newHashSetWithExpectedSize(users.size()); - for (User user : users) { - accountNames.add(user.getAccountName()); - } - return accountNames; - } - - /** - * checks actual current account against current accounts when the activity has been started. - * - * @return true if account list has changed, false if not - */ - private boolean hasCurrentAccountChanged() { - User user = getUserAccountManager().getUser(); - if (user.isAnonymous()) { - return true; - } else { - return !user.getAccountName().equals(originalCurrentUser); - } - } - - private List getUserListItems() { - List users = accountManager.getAllUsers(); - List userListItems = new ArrayList<>(users.size()); - for (User user : users) { - boolean pendingForRemoval = arbitraryDataProvider.getBooleanValue(user, PENDING_FOR_REMOVAL); - userListItems.add(new UserListItem(user, !pendingForRemoval)); - } - - if (getResources().getBoolean(R.bool.multiaccount_support)) { - userListItems.add(new UserListItem()); - } - - return userListItems; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - boolean retval = true; - - if (item.getItemId() == android.R.id.home) { - onBackPressed(); - } else { - retval = super.onOptionsItemSelected(item); - } - - return retval; - } - - @Override - public void showFirstRunActivity() { - Intent firstRunIntent = new Intent(getApplicationContext(), FirstRunActivity.class); - firstRunIntent.putExtra(FirstRunActivity.EXTRA_ALLOW_CLOSE, true); - startActivity(firstRunIntent); - } - - @Override - public void startAccountCreation() { - AccountManager am = AccountManager.get(getApplicationContext()); - am.addAccount(MainApp.getAccountType(this), - null, - null, - null, - this, - future -> { - if (future != null) { - try { - Bundle result = future.getResult(); - String name = result.getString(AccountManager.KEY_ACCOUNT_NAME); - accountManager.setCurrentOwnCloudAccount(name); - userListAdapter = new UserListAdapter( - this, - accountManager, - getUserListItems(), - this, - multipleAccountsSupported, - false, - true, - viewThemeUtils); - recyclerView.setAdapter(userListAdapter); - runOnUiThread(() -> userListAdapter.notifyDataSetChanged()); - } catch (OperationCanceledException e) { - Log_OC.d(TAG, "Account creation canceled"); - } catch (Exception e) { - Log_OC.e(TAG, "Account creation finished in exception: ", e); - } - } - }, handler); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onAccountRemovedEvent(AccountRemovedEvent event) { - List userListItemArray = getUserListItems(); - userListAdapter.clear(); - userListAdapter.addAll(userListItemArray); - userListAdapter.notifyDataSetChanged(); - } - - @Override - public void run(AccountManagerFuture future) { - if (future.isDone()) { - // after remove account - Optional user = accountManager.getUser(accountName); - if (!user.isPresent()) { - fileUploadHelper.cancel(accountName); - FileDownloadHelper.Companion.instance().cancelAllDownloadsForAccount(workerAccountName, workerCurrentDownload); - } - - User currentUser = getUserAccountManager().getUser(); - if (currentUser.isAnonymous()) { - String accountName = ""; - List users = accountManager.getAllUsers(); - if (users.size() > 0) { - accountName = users.get(0).getAccountName(); - } - accountManager.setCurrentOwnCloudAccount(accountName); - } - - List userListItemArray = getUserListItems(); - if (userListItemArray.size() > SINGLE_ACCOUNT) { - userListAdapter = new UserListAdapter(this, - accountManager, - userListItemArray, - this, - multipleAccountsSupported, - false, - true, - viewThemeUtils); - recyclerView.setAdapter(userListAdapter); - } else { - onBackPressed(); - } - } - } - - public Handler getHandler() { - return handler; - } - - @Override - public OperationsService.OperationsServiceBinder getOperationsServiceBinder() { - return null; - } - - @Override - public FileDataStorageManager getStorageManager() { - return super.getStorageManager(); - } - - @Override - public FileOperationsHelper getFileOperationsHelper() { - return null; - } - - private void performAccountRemoval(User user) { - // disable account in recycler view - for (int i = 0; i < userListAdapter.getItemCount(); i++) { - UserListItem item = userListAdapter.getItem(i); - - if (item != null && item.getUser().getAccountName().equalsIgnoreCase(user.getAccountName())) { - item.setEnabled(false); - break; - } - - userListAdapter.notifyDataSetChanged(); - } - - // store pending account removal - ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(this); - arbitraryDataProvider.storeOrUpdateKeyValue(user.getAccountName(), PENDING_FOR_REMOVAL, String.valueOf(true)); - - FileDownloadHelper.Companion.instance().cancelAllDownloadsForAccount(workerAccountName, workerCurrentDownload); - fileUploadHelper.cancel(user.getAccountName()); - backgroundJobManager.startAccountRemovalJob(user.getAccountName(), false); - - // immediately select a new account - List users = accountManager.getAllUsers(); - - String newAccountName = ""; - for (User u : users) { - if (!u.getAccountName().equalsIgnoreCase(u.getAccountName())) { - newAccountName = u.getAccountName(); - break; - } - } - - if (newAccountName.isEmpty()) { - Log_OC.d(TAG, "new account set to null"); - accountManager.resetOwnCloudAccount(); - } else { - Log_OC.d(TAG, "new account set to: " + newAccountName); - accountManager.setCurrentOwnCloudAccount(newAccountName); - } - - // only one to be (deleted) account remaining - if (users.size() < MIN_MULTI_ACCOUNT_SIZE) { - Intent resultIntent = new Intent(); - resultIntent.putExtra(KEY_ACCOUNT_LIST_CHANGED, true); - resultIntent.putExtra(KEY_CURRENT_ACCOUNT_CHANGED, true); - setResult(RESULT_OK, resultIntent); - - super.onBackPressed(); - } - } - - public static void openAccountRemovalDialog(User user, FragmentManager fragmentManager) { - AccountRemovalDialog dialog = AccountRemovalDialog.newInstance(user); - dialog.show(fragmentManager, "dialog"); - } - - private void openAccount(User user) { - final Intent intent = new Intent(this, UserInfoActivity.class); - intent.putExtra(UserInfoActivity.KEY_ACCOUNT, user); - OwnCloudAccount oca = user.toOwnCloudAccount(); - intent.putExtra(KEY_DISPLAY_NAME, oca.getDisplayName()); - startActivityForResult(intent, KEY_USER_INFO_REQUEST_CODE); - } - - @VisibleForTesting - public void showUser(User user, UserInfo userInfo) { - final Intent intent = new Intent(this, UserInfoActivity.class); - OwnCloudAccount oca = user.toOwnCloudAccount(); - intent.putExtra(UserInfoActivity.KEY_ACCOUNT, user); - intent.putExtra(KEY_DISPLAY_NAME, oca.getDisplayName()); - intent.putExtra(KEY_USER_DATA, userInfo); - startActivityForResult(intent, KEY_USER_INFO_REQUEST_CODE); - } - - @Override - public void onOptionItemClicked(User user, View view) { - if (view.getId() == R.id.account_menu) { - PopupMenu popup = new PopupMenu(this, view); - popup.getMenuInflater().inflate(R.menu.item_account, popup.getMenu()); - - if (accountManager.getUser().equals(user)) { - popup.getMenu().findItem(R.id.action_open_account).setVisible(false); - } - popup.setOnMenuItemClickListener(item -> { - int itemId = item.getItemId(); - - if (itemId == R.id.action_open_account) { - accountClicked(user.hashCode()); - } else if (itemId == R.id.action_delete_account) { - openAccountRemovalDialog(user, getSupportFragmentManager()); - } else { - openAccount(user); - } - - return true; - }); - popup.show(); - } else { - openAccount(user); - } - } - - private void observeWorkerState() { - WorkerStateLiveData.Companion.instance().observe(this, state -> { - if (state instanceof WorkerState.Download) { - Log_OC.d(TAG, "Download worker started"); - workerAccountName = ((WorkerState.Download) state).getUser().getAccountName(); - workerCurrentDownload = ((WorkerState.Download) state).getCurrentDownload(); - } - }); - } - - @Override - public void onAccountClicked(User user) { - openAccount(user); - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.kt new file mode 100644 index 0000000..ef3526e --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.kt @@ -0,0 +1,496 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2024 Alper Ozturk + * SPDX-FileCopyrightText: 2020 Chris Narkiewicz + * SPDX-FileCopyrightText: 2020 Chawki Chouib + * SPDX-FileCopyrightText: 2019 Tobias Kaminsky + * SPDX-FileCopyrightText: 2016-2018 Andy Scherzinger + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) + */ +package com.owncloud.android.ui.activity + +import android.accounts.Account +import android.accounts.AccountManager +import android.accounts.AccountManagerCallback +import android.accounts.AccountManagerFuture +import android.accounts.OperationCanceledException +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Bundle +import android.os.Handler +import android.view.MenuItem +import android.view.View +import androidx.activity.OnBackPressedCallback +import androidx.annotation.VisibleForTesting +import androidx.appcompat.widget.PopupMenu +import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.common.collect.Sets +import com.nextcloud.client.account.User +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.jobs.download.FileDownloadHelper +import com.nextcloud.client.onboarding.FirstRunActivity +import com.nextcloud.model.WorkerState +import com.nextcloud.model.WorkerState.DownloadStarted +import com.nextcloud.model.WorkerStateLiveData +import com.nextcloud.utils.extensions.getParcelableArgument +import com.nextcloud.utils.mdm.MDMConfig.multiAccountSupport +import com.owncloud.android.MainApp +import com.owncloud.android.R +import com.owncloud.android.authentication.AuthenticatorActivity +import com.owncloud.android.datamodel.ArbitraryDataProvider +import com.owncloud.android.datamodel.ArbitraryDataProviderImpl +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.lib.common.UserInfo +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.DownloadFileOperation +import com.owncloud.android.services.OperationsService.OperationsServiceBinder +import com.owncloud.android.ui.adapter.UserListAdapter +import com.owncloud.android.ui.adapter.UserListItem +import com.owncloud.android.ui.dialog.AccountRemovalDialog.Companion.newInstance +import com.owncloud.android.ui.events.AccountRemovedEvent +import com.owncloud.android.ui.helpers.FileOperationsHelper +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode + +/** + * An Activity that allows the user to manage accounts. + */ +class ManageAccountsActivity : + FileActivity(), + UserListAdapter.Listener, + AccountManagerCallback, + ComponentsGetter, + UserListAdapter.ClickListener { + + private var recyclerView: RecyclerView? = null + private val handler = Handler() + private var accountName: String? = null + private var userListAdapter: UserListAdapter? = null + private var originalUsers: Set? = null + private var originalCurrentUser: String? = null + + private var multipleAccountsSupported = false + + private var workerAccountName: String? = null + private var workerCurrentDownload: DownloadFileOperation? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.accounts_layout) + + setupToolbar() + setupActionBar() + setupUsers() + + @Suppress("DEPRECATION") + arbitraryDataProvider = ArbitraryDataProviderImpl(this) + multipleAccountsSupported = multiAccountSupport(this) + + setupUserList() + handleBackPress() + } + + private fun setupUsers() { + val users = accountManager.allUsers + originalUsers = toAccountNames(users) + + user.ifPresent { + originalCurrentUser = user.get().accountName + } + } + + private fun setupActionBar() { + supportActionBar?.let { + it.setDisplayHomeAsUpEnabled(true) + it.setDisplayShowHomeEnabled(true) + viewThemeUtils.files.themeActionBar(this, it, R.string.prefs_manage_accounts) + } + } + + private fun setupUserList() { + userListAdapter = UserListAdapter( + this, + accountManager, + userListItems, + this, + multipleAccountsSupported, + true, + true, + viewThemeUtils + ) + + recyclerView = findViewById(R.id.account_list) + recyclerView?.setAdapter(userListAdapter) + recyclerView?.setLayoutManager(LinearLayoutManager(this)) + observeWorkerState() + } + + @Suppress("ReturnCount") + @Deprecated("Use ActivityResultLauncher") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (resultCode != KEY_DELETE_CODE || data == null) { + return + } + + val bundle = data.extras + if (bundle == null || !bundle.containsKey(UserInfoActivity.KEY_ACCOUNT)) { + return + } + + val account = bundle.getParcelableArgument(UserInfoActivity.KEY_ACCOUNT, Account::class.java) ?: return + val user = accountManager.getUser(account.name).orElseThrow { RuntimeException() } + accountName = account.name + performAccountRemoval(user) + } + + private fun handleBackPress() { + onBackPressedDispatcher.addCallback( + this, + onBackPressedCallback + ) + } + + private val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + val resultIntent = Intent() + + if (accountManager.allUsers.isNotEmpty()) { + resultIntent.putExtra(KEY_ACCOUNT_LIST_CHANGED, hasAccountListChanged()) + resultIntent.putExtra(KEY_CURRENT_ACCOUNT_CHANGED, hasCurrentAccountChanged()) + setResult(RESULT_OK, resultIntent) + } else { + val intent = Intent(this@ManageAccountsActivity, AuthenticatorActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP + startActivity(intent) + } + + finish() + } + } + + /** + * checks the set of actual accounts against the set of original accounts when the activity has been started. + * + * @return true if account list has changed, false if not + */ + private fun hasAccountListChanged(): Boolean { + val users = accountManager.allUsers + val newList: MutableList = ArrayList() + for (user in users) { + val pendingForRemoval = arbitraryDataProvider.getBooleanValue(user, PENDING_FOR_REMOVAL) + + if (!pendingForRemoval) { + newList.add(user) + } + } + val actualAccounts = toAccountNames(newList) + return originalUsers != actualAccounts + } + + /** + * checks actual current account against current accounts when the activity has been started. + * + * @return true if account list has changed, false if not + */ + private fun hasCurrentAccountChanged(): Boolean { + val user = userAccountManager.user + return if (user.isAnonymous) { + true + } else { + user.accountName != originalCurrentUser + } + } + + private val userListItems: List + get() { + val users = accountManager.allUsers + val userListItems: MutableList = + ArrayList(users.size) + for (user in users) { + val pendingForRemoval = + arbitraryDataProvider.getBooleanValue(user, PENDING_FOR_REMOVAL) + userListItems.add(UserListItem(user, !pendingForRemoval)) + } + + if (multiAccountSupport(this)) { + userListItems.add(UserListItem()) + } + + return userListItems + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + var result = true + + if (item.itemId == android.R.id.home) { + onBackPressedDispatcher.onBackPressed() + } else { + result = super.onOptionsItemSelected(item) + } + + return result + } + + override fun showFirstRunActivity() { + val intent = Intent(applicationContext, FirstRunActivity::class.java).apply { + putExtra(FirstRunActivity.EXTRA_ALLOW_CLOSE, true) + } + startActivity(intent) + } + + @Suppress("TooGenericExceptionCaught") + @SuppressLint("NotifyDataSetChanged") + override fun startAccountCreation() { + val am = AccountManager.get(applicationContext) + am.addAccount( + MainApp.getAccountType(this), + null, + null, + null, + this, + { future: AccountManagerFuture? -> + if (future != null) { + try { + val result = future.result + val name = result.getString(AccountManager.KEY_ACCOUNT_NAME) + accountManager.setCurrentOwnCloudAccount(name) + userListAdapter = UserListAdapter( + this, + accountManager, + userListItems, + this, + multipleAccountsSupported, + false, + true, + viewThemeUtils + ) + recyclerView?.adapter = userListAdapter + runOnUiThread { userListAdapter?.notifyDataSetChanged() } + } catch (e: OperationCanceledException) { + Log_OC.d(TAG, "Account creation canceled") + } catch (e: Exception) { + Log_OC.e(TAG, "Account creation finished in exception: ", e) + } + } + }, + handler + ) + } + + @SuppressLint("NotifyDataSetChanged") + @Subscribe(threadMode = ThreadMode.MAIN) + override fun onAccountRemovedEvent(event: AccountRemovedEvent) { + val userListItemArray = userListItems + userListAdapter?.clear() + userListAdapter?.addAll(userListItemArray) + userListAdapter?.notifyDataSetChanged() + } + + override fun run(future: AccountManagerFuture) { + if (!future.isDone) { + return + } + + // after remove account + accountName?.let { + val user = accountManager.getUser(it) + + if (!user.isPresent) { + fileUploadHelper.cancel(it) + FileDownloadHelper.instance().cancelAllDownloadsForAccount(workerAccountName, workerCurrentDownload) + } + } + + val currentUser = userAccountManager.user + if (currentUser.isAnonymous) { + var accountName = "" + val users = accountManager.allUsers + if (users.size > 0) { + accountName = users[0].accountName + } + accountManager.setCurrentOwnCloudAccount(accountName) + } + + val userListItemArray = userListItems + if (userListItemArray.size > SINGLE_ACCOUNT) { + userListAdapter = UserListAdapter( + this, + accountManager, + userListItemArray, + this, + multipleAccountsSupported, + false, + true, + viewThemeUtils + ) + recyclerView?.adapter = userListAdapter + } else { + onBackPressedDispatcher.onBackPressed() + } + } + + override fun getHandler(): Handler = handler + + override fun getOperationsServiceBinder(): OperationsServiceBinder? = null + + override fun getStorageManager(): FileDataStorageManager = super.getStorageManager() + + override fun getFileOperationsHelper(): FileOperationsHelper? = null + + @Suppress("DEPRECATION") + @SuppressLint("NotifyDataSetChanged") + private fun performAccountRemoval(user: User) { + val itemCount = userListAdapter?.itemCount ?: 0 + + // disable account in recycler view + for (i in 0 until itemCount) { + val item = userListAdapter?.getItem(i) + + if (item != null && item.user.accountName.equals(user.accountName, ignoreCase = true)) { + item.isEnabled = false + break + } + + userListAdapter?.notifyDataSetChanged() + } + + // store pending account removal + val arbitraryDataProvider: ArbitraryDataProvider = ArbitraryDataProviderImpl(this) + arbitraryDataProvider.storeOrUpdateKeyValue(user.accountName, PENDING_FOR_REMOVAL, true.toString()) + + FileDownloadHelper.instance().cancelAllDownloadsForAccount(workerAccountName, workerCurrentDownload) + fileUploadHelper.cancel(user.accountName) + backgroundJobManager.startAccountRemovalJob(user.accountName, false) + + // immediately select a new account + val users = accountManager.allUsers + + var newAccountName = "" + for (u in users) { + if (!u.accountName.equals(u.accountName, ignoreCase = true)) { + newAccountName = u.accountName + break + } + } + + if (newAccountName.isEmpty()) { + Log_OC.d(TAG, "new account set to null") + accountManager.resetOwnCloudAccount() + } else { + Log_OC.d(TAG, "new account set to: $newAccountName") + accountManager.setCurrentOwnCloudAccount(newAccountName) + } + + // only one to be (deleted) account remaining + if (users.size < MIN_MULTI_ACCOUNT_SIZE) { + val resultIntent = Intent() + resultIntent.putExtra(KEY_ACCOUNT_LIST_CHANGED, true) + resultIntent.putExtra(KEY_CURRENT_ACCOUNT_CHANGED, true) + setResult(RESULT_OK, resultIntent) + onBackPressedDispatcher.onBackPressed() + } + } + + @Suppress("DEPRECATION") + private fun openAccount(user: User) { + val intent = Intent(this, UserInfoActivity::class.java).apply { + putExtra(UserInfoActivity.KEY_ACCOUNT, user) + + val oca = user.toOwnCloudAccount() + putExtra(UserListAdapter.KEY_DISPLAY_NAME, oca.displayName) + } + + startActivityForResult(intent, UserListAdapter.KEY_USER_INFO_REQUEST_CODE) + } + + @Suppress("DEPRECATION") + @VisibleForTesting + fun showUser(user: User, userInfo: UserInfo?) { + val intent = Intent(this, UserInfoActivity::class.java).apply { + val oca = user.toOwnCloudAccount() + putExtra(UserInfoActivity.KEY_ACCOUNT, user) + putExtra(UserListAdapter.KEY_DISPLAY_NAME, oca.displayName) + putExtra(UserInfoActivity.KEY_USER_DATA, userInfo) + } + + startActivityForResult(intent, UserListAdapter.KEY_USER_INFO_REQUEST_CODE) + } + + override fun onOptionItemClicked(user: User, view: View) { + if (view.id == R.id.account_menu) { + val popup = PopupMenu(this, view) + popup.menuInflater.inflate(R.menu.item_account, popup.menu) + + if (accountManager.user == user) { + popup.menu.findItem(R.id.action_open_account).setVisible(false) + } + + popup.setOnMenuItemClickListener { item: MenuItem -> + val itemId = item.itemId + when (itemId) { + R.id.action_open_account -> { + accountClicked(user.hashCode()) + } + R.id.action_delete_account -> { + openAccountRemovalDialog(user, supportFragmentManager) + } + else -> { + openAccount(user) + } + } + true + } + + popup.show() + } else { + openAccount(user) + } + } + + private fun observeWorkerState() { + WorkerStateLiveData.instance().observe( + this + ) { state: WorkerState? -> + if (state is DownloadStarted) { + Log_OC.d(TAG, "Download worker started") + workerAccountName = state.user?.accountName + workerCurrentDownload = state.currentDownload + } + } + } + + override fun onAccountClicked(user: User) { + openAccount(user) + } + + companion object { + private val TAG: String = ManageAccountsActivity::class.java.simpleName + + const val KEY_ACCOUNT_LIST_CHANGED: String = "ACCOUNT_LIST_CHANGED" + const val KEY_CURRENT_ACCOUNT_CHANGED: String = "CURRENT_ACCOUNT_CHANGED" + const val PENDING_FOR_REMOVAL: String = UserAccountManager.PENDING_FOR_REMOVAL + + private const val KEY_DELETE_CODE = 101 + private const val SINGLE_ACCOUNT = 1 + private const val MIN_MULTI_ACCOUNT_SIZE = 2 + + private fun toAccountNames(users: Collection): Set { + val accountNames: MutableSet = Sets.newHashSetWithExpectedSize(users.size) + for (user in users) { + accountNames.add(user.accountName) + } + return accountNames + } + + fun openAccountRemovalDialog(user: User, fragmentManager: FragmentManager) { + val dialog = newInstance(user) + dialog.show(fragmentManager, "dialog") + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt index 909dd0d..510d5be 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/ManageSpaceActivity.kt @@ -1,28 +1,34 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2025 Alper Ozturk * SPDX-FileCopyrightText: 2022 Álvaro Brey - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only */ package com.owncloud.android.ui.activity -import android.os.AsyncTask import android.os.Bundle import android.view.MenuItem -import android.widget.Button -import android.widget.TextView import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar +import com.nextcloud.android.common.ui.util.extensions.applyEdgeToEdgeWithSystemBarPadding import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.di.Injectable import com.nextcloud.client.preferences.AppPreferences import com.owncloud.android.R +import com.owncloud.android.databinding.ActivityManageSpaceBinding import com.owncloud.android.lib.common.utils.Log_OC +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.File import javax.inject.Inject import kotlin.system.exitProcess -class ManageSpaceActivity : AppCompatActivity(), Injectable { +class ManageSpaceActivity : + AppCompatActivity(), + Injectable { @Inject lateinit var preferences: AppPreferences @@ -30,44 +36,31 @@ class ManageSpaceActivity : AppCompatActivity(), Injectable { @Inject lateinit var userAccountManager: UserAccountManager - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_manage_space) - val actionBar = supportActionBar - if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(true) - actionBar.setTitle(R.string.manage_space_title) - } - val descriptionTextView = findViewById(R.id.general_description) - descriptionTextView.text = getString(R.string.manage_space_description, getString(R.string.app_name)) - val clearDataButton = findViewById