mirror of
https://github.com/bitfireAT/davx5-ose.git
synced 2026-08-07 05:44:54 -04:00
Compare commits
98
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70973dcc0a | ||
|
|
a511b16211 | ||
|
|
46fb5df191 | ||
|
|
72e31a7330 | ||
|
|
2a6cfc5346 | ||
|
|
6c4acad758 | ||
|
|
c7fe069b1f | ||
|
|
972da39e4a | ||
|
|
c5a4958d3a | ||
|
|
a7aec7b4a9 | ||
|
|
ea31f5a621 | ||
|
|
ea06c4a7a1 | ||
|
|
fee6431981 | ||
|
|
9b992c54ca | ||
|
|
4073880185 | ||
|
|
b3f145f6a8 | ||
|
|
4f1488ece5 | ||
|
|
4dc65d0144 | ||
|
|
43cc5c0e7b | ||
|
|
983214d23b | ||
|
|
520ec21d9e | ||
|
|
e667de8992 | ||
|
|
8be6fdedd9 | ||
|
|
df012efe78 | ||
|
|
558075f1bb | ||
|
|
4505a5958d | ||
|
|
385e7b5e13 | ||
|
|
64068f7007 | ||
|
|
f4bb3639e6 | ||
|
|
8a651f135b | ||
|
|
6cfaad35b1 | ||
|
|
ec4fedf04e | ||
|
|
d30715f813 | ||
|
|
3af25754c4 | ||
|
|
15e8e20bad | ||
|
|
b35273a6d5 | ||
|
|
33b8aa8e05 | ||
|
|
eaa080cfe8 | ||
|
|
8c79c64d75 | ||
|
|
87b71b0b38 | ||
|
|
725c815e32 | ||
|
|
1986837be8 | ||
|
|
44cf628dd6 | ||
|
|
3cbfcbee9b | ||
|
|
8f7b48748b | ||
|
|
4f9bd5bebd | ||
|
|
0f09881eaf | ||
|
|
759e32b89b | ||
|
|
55d9dc4357 | ||
|
|
37ff088b78 | ||
|
|
af698c584b | ||
|
|
1fc2679bd4 | ||
|
|
66d6ec34d1 | ||
|
|
b802ed07d9 | ||
|
|
6889559fef | ||
|
|
8e8bc1c772 | ||
|
|
e7e7759f41 | ||
|
|
1e00ed217f | ||
|
|
f8f10be3ba | ||
|
|
5aa77e51a2 | ||
|
|
c1f57a4470 | ||
|
|
477a3c89f7 | ||
|
|
ab517bd87d | ||
|
|
c753dcaa8e | ||
|
|
362f0036be | ||
|
|
0626f1ecdc | ||
|
|
f85dbe934b | ||
|
|
603758fcbc | ||
|
|
fb671a67a4 | ||
|
|
ff4fe77fe3 | ||
|
|
da4443da62 | ||
|
|
a45aed6914 | ||
|
|
dbc89cb4fe | ||
|
|
b6611131db | ||
|
|
e3ba460991 | ||
|
|
68bd43fe6a | ||
|
|
b2ab24afd2 | ||
|
|
a01c531784 | ||
|
|
42929ddcf1 | ||
|
|
03a85da6e3 | ||
|
|
d15478b863 | ||
|
|
5db8bcb9d8 | ||
|
|
c87fb7bedd | ||
|
|
59ded55ac1 | ||
|
|
db1f2666e8 | ||
|
|
6ee010905e | ||
|
|
b9a13bf7de | ||
|
|
7ab740d443 | ||
|
|
9a9d9709c9 | ||
|
|
6727987051 | ||
|
|
5d3ef61b77 | ||
|
|
62ed38ee4f | ||
|
|
c012512de1 | ||
|
|
495b854bf2 | ||
|
|
3729951b52 | ||
|
|
350171c20c | ||
|
|
90cf92bc16 | ||
|
|
f92a3d77b5 |
No files matched your search
@@ -1,4 +0,0 @@
|
||||
|
||||
github: bitfireAT
|
||||
liberapay: DAVx5
|
||||
custom: 'https://www.davx5.com/donate'
|
||||
@@ -1,5 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: DAVx⁵ Community Support
|
||||
url: https://github.com/bitfireAT/davx5-ose/discussions
|
||||
about: Ask and answer questions (including feature requests and bug reports) here.
|
||||
@@ -1,36 +0,0 @@
|
||||
|
||||
The PR should be in _Draft_ state during development. As soon as it's finished, it should be marked as _Ready for review_ and a reviewer should be chosen.
|
||||
|
||||
See also: [Writing A Great Pull Request Description](https://www.pullrequest.com/blog/writing-a-great-pull-request-description/)
|
||||
|
||||
|
||||
### Purpose
|
||||
|
||||
What this PR is intended to do and why this is desirable.
|
||||
|
||||
Example:
|
||||
|
||||
> Adds support for AAA in BBB, as requested by several people in issue #XX.
|
||||
|
||||
|
||||
### Short description
|
||||
|
||||
A short description of the chosen approach to achieve the purpose.
|
||||
|
||||
Example:
|
||||
|
||||
> - Added authentication option _Some authentication_ to _some module_.
|
||||
> - Added support for _Some authentication_ to _some content provider_.
|
||||
> - Added UI support for _Some authentication_ in account settings.
|
||||
|
||||
Related information (links to Android docs and other resources that help to understand/review
|
||||
the changes) can also be put here.
|
||||
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] The PR has a proper title, description and label.
|
||||
- [ ] I have [self-reviewed the PR](https://patrickdinh.medium.com/review-your-own-pull-requests-5634cad10b7a).
|
||||
- [ ] I have added documentation to complex functions and functions that can be used by other modules.
|
||||
- [ ] I have added reasonable tests or consciously decided to not add tests.
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- ignore-for-release
|
||||
categories:
|
||||
- title: New features
|
||||
labels:
|
||||
- enhancement
|
||||
- title: Bug fixes
|
||||
labels:
|
||||
- bug
|
||||
- title: Refactoring
|
||||
labels:
|
||||
- refactoring
|
||||
- title: Other changes
|
||||
labels:
|
||||
- "*"
|
||||
@@ -1,65 +0,0 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main-ose ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main-ose ]
|
||||
schedule:
|
||||
- cron: '22 10 * * 1'
|
||||
concurrency:
|
||||
group: codeql-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'java' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
- uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.gradle_encryption_key }}
|
||||
cache-read-only: true # gradle user home cache is generated by test jobs
|
||||
|
||||
- name: Use incremental build cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
key: incremental-build-tests
|
||||
restore-keys: incremental-build-tests # restore cache from main branch
|
||||
path: app/build
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
#- name: Autobuild
|
||||
# uses: github/codeql-action/autobuild@v2
|
||||
|
||||
- name: Build
|
||||
run: ./gradlew --build-cache --configuration-cache --configuration-cache-problems=warn --no-daemon app:assembleOseDebug
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
@@ -1,55 +0,0 @@
|
||||
name: Dependent Issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- closed
|
||||
- reopened
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- closed
|
||||
- reopened
|
||||
# Makes sure we always add status check for PRs. Useful only if
|
||||
# this action is required to pass before merging. Otherwise, it
|
||||
# can be removed.
|
||||
- synchronize
|
||||
|
||||
# Schedule a daily check. Useful if you reference cross-repository
|
||||
# issues or pull requests. Otherwise, it can be removed.
|
||||
schedule:
|
||||
- cron: '19 9 * * *'
|
||||
|
||||
permissions: write-all
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: z0al/dependent-issues@v1
|
||||
env:
|
||||
# (Required) The token to use to make API calls to GitHub.
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# (Optional) The token to use to make API calls to GitHub for remote repos.
|
||||
GITHUB_READ_TOKEN: ${{ secrets.DEPENDENT_ISSUES_READ_TOKEN }}
|
||||
|
||||
with:
|
||||
# (Optional) The label to use to mark dependent issues
|
||||
# label: dependent
|
||||
|
||||
# (Optional) Enable checking for dependencies in issues.
|
||||
# Enable by setting the value to "on". Default "off"
|
||||
check_issues: on
|
||||
|
||||
# (Optional) A comma-separated list of keywords. Default
|
||||
# "depends on, blocked by"
|
||||
keywords: depends on, blocked by
|
||||
|
||||
# (Optional) A custom comment body. It supports `{{ dependencies }}` token.
|
||||
comment: >
|
||||
This PR/issue depends on:
|
||||
|
||||
{{ dependencies }}
|
||||
@@ -1,48 +0,0 @@
|
||||
name: Create release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
prerelease: ${{ contains(github.ref_name, '-alpha') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-rc') }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Create release
|
||||
permissions:
|
||||
contents: write
|
||||
discussions: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
- uses: gradle/actions/setup-gradle@v3
|
||||
|
||||
- name: Prepare keystore
|
||||
run: echo ${{ secrets.android_keystore_base64 }} | base64 -d >$GITHUB_WORKSPACE/keystore.jks
|
||||
|
||||
- name: Build signed package
|
||||
# Make sure that caches are disabled to generate reproducible release builds
|
||||
run: ./gradlew --no-build-cache --no-configuration-cache --no-daemon app:assembleRelease
|
||||
env:
|
||||
ANDROID_KEYSTORE: ${{ github.workspace }}/keystore.jks
|
||||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.android_keystore_password }}
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.android_key_alias }}
|
||||
ANDROID_KEY_PASSWORD: ${{ secrets.android_key_password }}
|
||||
|
||||
- name: Create Github release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
prerelease: ${{ env.prerelease }}
|
||||
files: app/build/outputs/apk/ose/release/*.apk
|
||||
fail_on_unmatched_files: true
|
||||
generate_release_notes: true
|
||||
discussion_category_name: Announcements
|
||||
@@ -1,84 +0,0 @@
|
||||
name: Development tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: test-dev-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
is_main_branch: ${{ github.ref == 'refs/heads/main-ose' }}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Tests without emulator
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
- uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.gradle_encryption_key }}
|
||||
cache-read-only: true # gradle user home cache is generated by test_on_emulator
|
||||
|
||||
- name: Use incremental build cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
key: incremental-build-tests
|
||||
restore-keys: incremental-build-tests # restore cache from main branch
|
||||
path: app/build
|
||||
|
||||
- name: Run lint and unit tests
|
||||
run: ./gradlew --build-cache --configuration-cache --configuration-cache-problems=warn --no-daemon app:check
|
||||
|
||||
# generates the build caches because it uses more gradle dependencies
|
||||
test_on_emulator:
|
||||
name: Tests with emulator
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
- uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.gradle_encryption_key }}
|
||||
gradle-home-cache-cleanup: true # avoid ever-growing gradle user home cache
|
||||
|
||||
- name: Use incremental build cache
|
||||
if: ${{ !env.is_main_branch }}
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
key: incremental-build-tests
|
||||
restore-keys: incremental-build-tests # restore cache from main branch
|
||||
path: |
|
||||
.gradle/configuration-cache
|
||||
app/build
|
||||
|
||||
- name: Enable KVM group perms
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: Cache AVD
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.config/.android/avd
|
||||
key: avd-${{ hashFiles('app/build.gradle.kts') }} # gradle-managed devices are defined there
|
||||
|
||||
- name: Run device tests
|
||||
run: ./gradlew --build-cache --configuration-cache --configuration-cache-problems=warn --no-daemon app:virtualCheck
|
||||
|
||||
- name: Create incremental build cache
|
||||
if: ${{ env.is_main_branch }}
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
key: incremental-build-tests-${{ github.run_id }}
|
||||
path: app/build
|
||||
+34
-66
@@ -1,92 +1,60 @@
|
||||
# Created by https://www.gitignore.io
|
||||
### ANDROID
|
||||
|
||||
### Android ###
|
||||
# Built application files
|
||||
# built application files
|
||||
*.apk
|
||||
*.ap_
|
||||
|
||||
# Files for the Dalvik VM
|
||||
# files for the dex VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Generated files
|
||||
# generated files
|
||||
bin/
|
||||
gen/
|
||||
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Eclipse project files
|
||||
.classpath
|
||||
.project
|
||||
|
||||
# Proguard folder generated by Eclipse
|
||||
proguard/
|
||||
|
||||
# Log Files
|
||||
*.log
|
||||
|
||||
|
||||
### Intellij ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
|
||||
|
||||
# Intellij project files
|
||||
*.iml
|
||||
|
||||
## Directory-based project format:
|
||||
.idea/
|
||||
# if you remove the above rule, at least ignore the following:
|
||||
|
||||
# User-specific stuff:
|
||||
# .idea/workspace.xml
|
||||
# .idea/tasks.xml
|
||||
# .idea/dictionaries
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
# .idea/dataSources.ids
|
||||
# .idea/dataSources.xml
|
||||
# .idea/sqlDataSources.xml
|
||||
# .idea/dynamic.xml
|
||||
# .idea/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
# .idea/gradle.xml
|
||||
# .idea/libraries
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
# .idea/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
.idea/
|
||||
|
||||
|
||||
### Gradle ###
|
||||
.gradle
|
||||
build/
|
||||
### ECLIPSE
|
||||
|
||||
# Ignore Gradle GUI config
|
||||
gradle-app.setting
|
||||
*.pydevproject
|
||||
.metadata
|
||||
bin/**
|
||||
tmp/**
|
||||
tmp/**/*
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~.nib
|
||||
local.properties
|
||||
.classpath
|
||||
.settings/
|
||||
.loadpath
|
||||
|
||||
### external libs ###
|
||||
.svn
|
||||
# External tool builders
|
||||
.externalToolBuilders/
|
||||
|
||||
# Javadoc
|
||||
javadoc/
|
||||
# Locally stored "Eclipse launch configurations"
|
||||
*.launch
|
||||
|
||||
# CDT-specific
|
||||
.cproject
|
||||
|
||||
# PDT-specific
|
||||
.buildpath
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = ar_SA: ar, en_GB: en-rGB, fa_IR: fa-rIR, fi_FI: fi, nb_NO: nb, sk_SK: sk, sl_SI: sl, tr_TR: tr, zh_CN: zh, zh_TW: zh-rTW
|
||||
|
||||
[o:bitfireAT:p:davx5:r:app]
|
||||
file_filter = app/src/main/res/values-<lang>/strings.xml
|
||||
source_file = app/src/main/res/values/strings.xml
|
||||
source_lang = en
|
||||
type = ANDROID
|
||||
minimum_perc = 20
|
||||
resource_name = App strings (all flavors)
|
||||
|
||||
|
||||
# Attention: fastlane directories are like "en-us", not "en-rUS"!
|
||||
|
||||
[o:bitfireAT:p:davx5:r:metadata-short-description]
|
||||
file_filter = fastlane/metadata/android/<lang>/short_description.txt
|
||||
source_file = fastlane/metadata/android/en-US/short_description.txt
|
||||
source_lang = en
|
||||
type = TXT
|
||||
minimum_perc = 100
|
||||
resource_name = Metadata: short description
|
||||
|
||||
[o:bitfireAT:p:davx5:r:metadata-full-description]
|
||||
file_filter = fastlane/metadata/android/<lang>/full_description.txt
|
||||
source_file = fastlane/metadata/android/en-US/full_description.txt
|
||||
source_lang = en
|
||||
type = TXT
|
||||
minimum_perc = 100
|
||||
resource_name = Metadata: full description
|
||||
@@ -1,11 +0,0 @@
|
||||
# This is the list of significant contributors to DAVx5.
|
||||
#
|
||||
# This does not necessarily list everyone who has contributed work.
|
||||
# To see the full list of contributors, see the revision history in
|
||||
# source control.
|
||||
|
||||
Ricki Hirner (bitfire.at)
|
||||
Bernhard Stockmann (bitfire.at)
|
||||
|
||||
Sunik Kupfer (bitfire.at)
|
||||
Patrick Lang (techbee.at)
|
||||
@@ -0,0 +1,77 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="at.bitfire.davdroid"
|
||||
android:versionCode="29"
|
||||
android:versionName="0.5.8.1" android:installLocation="internalOnly">
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="14"
|
||||
android:targetSdkVersion="19" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.READ_CALENDAR" />
|
||||
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme" >
|
||||
<service
|
||||
android:name=".syncadapter.AccountAuthenticatorService"
|
||||
android:exported="false" >
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/account_authenticator" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".syncadapter.ContactsSyncAdapterService"
|
||||
android:exported="true"
|
||||
android:process=":sync" >
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_contacts" />
|
||||
<meta-data
|
||||
android:name="android.provider.CONTACTS_STRUCTURE"
|
||||
android:resource="@xml/contacts" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".syncadapter.CalendarsSyncAdapterService"
|
||||
android:exported="true"
|
||||
android:process=":sync" >
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_calendars" />
|
||||
</service>
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".syncadapter.AddAccountActivity"
|
||||
android:excludeFromRecents="true" >
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
|
||||
**Thank you for your interest in contributing to DAVx⁵!**
|
||||
|
||||
|
||||
# Licensing
|
||||
|
||||
All work in this repository is [licensed under the GPLv3](LICENSE).
|
||||
|
||||
We (bitfire.at, initial and main contributors) are also asking you to give us
|
||||
permission to use your contribution for related non-open source projects
|
||||
like [Managed DAVx⁵](https://www.davx5.com/organizations/managed-davx5).
|
||||
|
||||
If you send us a pull request, our CLA bot will ask you to sign the
|
||||
Contributor's License Agreement so that we can use your contribution.
|
||||
|
||||
|
||||
# Copyright
|
||||
|
||||
Make sure that every file that contains significant work (at least every code file)
|
||||
starts with the copyright header:
|
||||
|
||||
```
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
```
|
||||
|
||||
You can set this in Android Studio:
|
||||
|
||||
1. Settings / Editor / Copyright / Copyright Profiles
|
||||
2. Paste the text above (without the stars).
|
||||
3. Set Formatting so that the preview exactly looks like above; one blank line after the block.
|
||||
4. Set this copyright profile as the default profile for the project.
|
||||
5. Apply copyright: right-click in file tree / Update copyright.
|
||||
|
||||
|
||||
# Style guide
|
||||
|
||||
Please adhere to the [Kotlin style guide](https://developer.android.com/kotlin/style-guide) and
|
||||
the following hints to make the source code uniform.
|
||||
|
||||
**Have a look at similar files and copy their style if you're not certain.**
|
||||
|
||||
Sample file (pay attention to blank lines and other formatting):
|
||||
|
||||
```
|
||||
<Copyright header, see above>
|
||||
|
||||
class MyClass(int arg1) : SuperClass() {
|
||||
|
||||
companion object {
|
||||
|
||||
const val CONSTANT_STRING = "Constant String";
|
||||
|
||||
fun staticMethod() { // Use static methods when you don't need the object context.
|
||||
// …
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var someProperty: String = "12345"
|
||||
var someRelatedProperty: Int = 12345
|
||||
|
||||
init {
|
||||
// constructor
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Use KDoc to document important methods. Don't use it dogmatically, but writing proper documentation
|
||||
* (not just the method name with spaces) helps you to re-think what the method shall really do.
|
||||
*/
|
||||
fun aFun1() { // Group methods by some logic (for instance, the order in which they will be called)
|
||||
} // and alphabetically within a group.
|
||||
|
||||
fun anotherFun() {
|
||||
// …
|
||||
}
|
||||
|
||||
|
||||
fun somethingCompletelyDifferent() { // two blank lines to separate groups
|
||||
}
|
||||
|
||||
fun helperForSomethingCompletelyDifferent() {
|
||||
someCall(arg1, arg2, arg3, arg4) // function calls: stick to one line unless it becomes confusing
|
||||
}
|
||||
|
||||
|
||||
class Model( // two blank lines before inner classes
|
||||
someArgument: SomeLongClass, // arguments in multiple lines when they're too long for one line
|
||||
anotherArgument: AnotherLongType,
|
||||
thirdArgument: AnotherLongTypeName
|
||||
) : ViewModel() {
|
||||
|
||||
fun abc() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
In general, use one blank line to separate things within one group of things, and two blank lines
|
||||
to separate groups. In rare cases, when methods are tightly coupled and are only helpers for another
|
||||
method, they may follow the calling method without separating blank lines.
|
||||
|
||||
## Tests
|
||||
|
||||
Test classes should be in the appropriate directory (see existing tests) and in the same package as the
|
||||
tested class. Tests are usually be named like `methodToBeTested_Condition()`, see
|
||||
[Test apps on Android](https://developer.android.com/training/testing/).
|
||||
|
||||
|
||||
# Authors
|
||||
|
||||
If you make significant contributions, feel free to add yourself to the [AUTHORS file](AUTHORS).
|
||||
|
||||
File renamed without changes.
@@ -1,46 +1,12 @@
|
||||
|
||||
[](https://www.davx5.com/)
|
||||
[](https://f-droid.org/packages/at.bitfire.davdroid/)
|
||||
[](https://github.com/bitfireAT/davx5-ose/blob/main/LICENSE)
|
||||
[](https://fosstodon.org/@davx5app)
|
||||
[](https://github.com/bitfireAT/davx5-ose/actions/workflows/test-dev.yml)
|
||||
|
||||

|
||||
|
||||
|
||||
DAVx⁵
|
||||
========
|
||||
|
||||
Please see the [DAVx⁵ Web site](https://www.davx5.com) for
|
||||
comprehensive information about DAVx⁵, including a list of services it has been tested with.
|
||||
|
||||
DAVx⁵ is licensed under the [GPLv3 License](LICENSE).
|
||||
|
||||
News and updates:
|
||||
|
||||
* [@davx5app@fosstodon.org](https://fosstodon.org/@davx5app) on Mastodon
|
||||
|
||||
**Help, feature requests, bug reports: [DAVx⁵ discussions](https://github.com/bitfireAT/davx5-ose/discussions)**
|
||||
|
||||
Parts of DAVx⁵ have been outsourced into these libraries:
|
||||
|
||||
* [cert4android](https://github.com/bitfireAT/cert4android) – custom certificate management
|
||||
* [dav4jvm](https://github.com/bitfireAT/dav4jvm) – WebDAV/CalDav/CardDAV framework
|
||||
* [ical4android](https://github.com/bitfireAT/ical4android) – iCalendar processing and Calendar Provider access
|
||||
* [vcard4android](https://github.com/bitfireAT/vcard4android) – vCard processing and Contacts Provider access
|
||||
|
||||
**If you want to support DAVx⁵, please consider [donating to DAVx⁵](https://www.davx5.com/donate)
|
||||
or [purchasing it](https://www.davx5.com/download).**
|
||||
Please see the [DAVdroid Web site](http://davdroid.bitfire.at).
|
||||
|
||||
|
||||
USED THIRD-PARTY LIBRARIES
|
||||
==========================
|
||||
|
||||
The most important libraries which are used by DAVx⁵ (alphabetically):
|
||||
* [iCal4j](http://ical4j.sourceforge.net/) ([New BSD License](http://sourceforge.net/p/ical4j/ical4j/ci/default/tree/LICENSE))
|
||||
* [ez-vcard](https://code.google.com/p/ez-vcard/) ([New BSD License](http://opensource.org/licenses/BSD-3-Clause))
|
||||
* [Simple XML Serialization](http://simple.sourceforge.net/) ([Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0))
|
||||
* [Project Lombok](http://projectlombok.org/) ([MIT License](http://opensource.org/licenses/mit-license.php))
|
||||
|
||||
* [dnsjava](https://github.com/dnsjava/dnsjava) – [BSD License](https://github.com/dnsjava/dnsjava/blob/master/LICENSE)
|
||||
* [ez-vcard](https://github.com/mangstadt/ez-vcard) – [New BSD License](https://github.com/mangstadt/ez-vcard/blob/master/LICENSE)
|
||||
* [iCal4j](https://github.com/ical4j/ical4j) – [New BSD License](https://github.com/ical4j/ical4j/blob/develop/LICENSE.txt)
|
||||
* [okhttp](https://square.github.io/okhttp) – [Apache License, Version 2.0](https://square.github.io/okhttp/#license)
|
||||
|
||||
See _About / Libraries_ in the app for all used libraries and their licenses.
|
||||
@@ -1,5 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security vulnerabilities using our [secure support form](https://www.davx5.com/support) or via email to support-en@davx5.com.
|
||||
@@ -1,2 +0,0 @@
|
||||
build
|
||||
target
|
||||
@@ -1,222 +0,0 @@
|
||||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.mikepenz.aboutLibraries)
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.ksp)
|
||||
}
|
||||
|
||||
// Android configuration
|
||||
android {
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "at.bitfire.davdroid"
|
||||
|
||||
versionCode = 403160100
|
||||
versionName = "4.3.16.1"
|
||||
|
||||
buildConfigField("long", "buildTime", "${System.currentTimeMillis()}L")
|
||||
|
||||
setProperty("archivesBaseName", "davx5-ose-" + versionName)
|
||||
|
||||
minSdk = 24 // Android 7.0
|
||||
targetSdk = 34 // Android 14
|
||||
|
||||
buildConfigField("String", "userAgent", "\"DAVx5\"")
|
||||
|
||||
testInstrumentationRunner = "at.bitfire.davdroid.CustomTestRunner"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
// enable because ical4android requires desugaring
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
compose = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
|
||||
}
|
||||
|
||||
// Java namespace for our classes (not to be confused with Android package ID)
|
||||
namespace = "at.bitfire.davdroid"
|
||||
|
||||
flavorDimensions += "distribution"
|
||||
productFlavors {
|
||||
create("ose") {
|
||||
dimension = "distribution"
|
||||
versionNameSuffix = "-ose"
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("androidTest") {
|
||||
assets.srcDir("$projectDir/schemas")
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("bitfire") {
|
||||
storeFile = file(System.getenv("ANDROID_KEYSTORE") ?: "/dev/null")
|
||||
storePassword = System.getenv("ANDROID_KEYSTORE_PASSWORD")
|
||||
keyAlias = System.getenv("ANDROID_KEY_ALIAS")
|
||||
keyPassword = System.getenv("ANDROID_KEY_PASSWORD")
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
getByName("release") {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules-release.pro")
|
||||
|
||||
isShrinkResources = true
|
||||
|
||||
signingConfig = signingConfigs.findByName("bitfire")
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
disable += arrayOf("GoogleAppIndexingWarning", "ImpliedQuantity", "MissingQuantity", "MissingTranslation", "ExtraTranslation", "RtlEnabled", "RtlHardcoded", "Typos", "NullSafeMutableLiveData")
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += arrayOf("META-INF/*.md")
|
||||
}
|
||||
}
|
||||
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
testOptions {
|
||||
managedDevices {
|
||||
localDevices {
|
||||
create("virtual") {
|
||||
device = "Pixel 3"
|
||||
apiLevel = 34
|
||||
systemImageSource = "aosp-atd"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
|
||||
configurations {
|
||||
configureEach {
|
||||
// exclude modules which are in conflict with system libraries
|
||||
exclude(module="commons-logging")
|
||||
exclude(group="org.json", module="json")
|
||||
|
||||
// Groovy requires SDK 26+, and it's not required, so exclude it
|
||||
exclude(group="org.codehaus.groovy")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// core
|
||||
implementation(libs.kotlin.stdlib)
|
||||
implementation(libs.kotlinx.coroutines)
|
||||
coreLibraryDesugaring(libs.android.desugaring)
|
||||
|
||||
// Hilt
|
||||
implementation(libs.hilt.android.base)
|
||||
ksp(libs.androidx.hilt.compiler)
|
||||
ksp(libs.hilt.android.compiler)
|
||||
|
||||
// support libs
|
||||
implementation(libs.androidx.activityCompose)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.browser)
|
||||
implementation(libs.androidx.cardView)
|
||||
implementation(libs.androidx.concurrentFuture)
|
||||
implementation(libs.androidx.constraintLayout)
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.androidx.fragment)
|
||||
implementation(libs.androidx.hilt.work)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.base)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.paging)
|
||||
implementation(libs.androidx.paging.compose)
|
||||
implementation(libs.androidx.preference)
|
||||
implementation(libs.androidx.security)
|
||||
implementation(libs.androidx.swiperefreshlayout)
|
||||
implementation(libs.androidx.work.base)
|
||||
implementation(libs.android.flexbox)
|
||||
implementation(libs.android.material)
|
||||
|
||||
// Jetpack Compose
|
||||
implementation(libs.compose.accompanist.permissions)
|
||||
implementation(platform(libs.compose.bom))
|
||||
implementation(libs.compose.material)
|
||||
implementation(libs.compose.materialIconsExtended)
|
||||
implementation(libs.compose.runtime.livedata)
|
||||
debugImplementation(libs.compose.ui.tooling)
|
||||
implementation(libs.compose.ui.toolingPreview)
|
||||
|
||||
// Glance Widgets
|
||||
implementation(libs.glance.base)
|
||||
implementation(libs.glance.material)
|
||||
|
||||
// Jetpack Room
|
||||
implementation(libs.room.runtime)
|
||||
implementation(libs.room.base)
|
||||
implementation(libs.room.paging)
|
||||
ksp(libs.room.compiler)
|
||||
|
||||
// own libraries
|
||||
implementation(libs.bitfire.cert4android)
|
||||
implementation(libs.bitfire.dav4jvm) {
|
||||
exclude(group="junit")
|
||||
}
|
||||
implementation(libs.bitfire.ical4android)
|
||||
implementation(libs.bitfire.vcard4android)
|
||||
|
||||
// third-party libs
|
||||
implementation(libs.appintro)
|
||||
implementation(libs.commons.collections)
|
||||
@Suppress("RedundantSuppression")
|
||||
implementation(libs.commons.io)
|
||||
implementation(libs.commons.lang)
|
||||
implementation(libs.commons.text)
|
||||
@Suppress("RedundantSuppression")
|
||||
implementation(libs.dnsjava)
|
||||
implementation(libs.mikepenz.aboutLibraries)
|
||||
implementation(libs.nsk90.kstatemachine)
|
||||
implementation(libs.okhttp.base)
|
||||
implementation(libs.okhttp.brotli)
|
||||
implementation(libs.okhttp.logging)
|
||||
implementation(libs.openid.appauth)
|
||||
|
||||
// for tests
|
||||
androidTestImplementation(libs.androidx.arch.core.testing)
|
||||
androidTestImplementation(libs.androidx.test.core)
|
||||
androidTestImplementation(libs.androidx.test.junit)
|
||||
androidTestImplementation(libs.androidx.test.rules)
|
||||
androidTestImplementation(libs.androidx.test.runner)
|
||||
androidTestImplementation(libs.androidx.work.testing)
|
||||
androidTestImplementation(libs.hilt.android.testing)
|
||||
androidTestImplementation(libs.junit)
|
||||
androidTestImplementation(libs.mockk.android)
|
||||
androidTestImplementation(libs.okhttp.mockwebserver)
|
||||
androidTestImplementation(libs.room.testing)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.okhttp.mockwebserver)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright (c) 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
||||
~ All rights reserved. This program and the accompanying materials
|
||||
~ are made available under the terms of the GNU Public License v3.0
|
||||
~ which accompanies this distribution, and is available at
|
||||
~ http://www.gnu.org/licenses/gpl.html
|
||||
-->
|
||||
|
||||
<lint>
|
||||
<issue id="InvalidPackage" severity="ignore" />
|
||||
<issue id="MissingTranslation" severity="warning" />
|
||||
</lint>
|
||||
@@ -1,58 +0,0 @@
|
||||
|
||||
# R8 usage for DAVx⁵:
|
||||
# shrinking yes (only in release builds)
|
||||
# optimization yes (on by R8 defaults)
|
||||
# full-mode no (see gradle.properties)
|
||||
# obfuscation no (open-source)
|
||||
|
||||
-dontobfuscate
|
||||
-printusage build/reports/r8-usage.txt
|
||||
|
||||
# ez-vcard: keep all vCard properties/parameters (used via reflection)
|
||||
-keep class ezvcard.io.scribe.** { *; }
|
||||
-keep class ezvcard.property.** { *; }
|
||||
-keep class ezvcard.parameter.** { *; }
|
||||
|
||||
# ical4j: keep all iCalendar properties/parameters (used via reflection)
|
||||
-keep class net.fortuna.ical4j.** { *; }
|
||||
|
||||
# XmlPullParser
|
||||
-keep class org.xmlpull.** { *; }
|
||||
|
||||
# DAVx⁵ + libs
|
||||
-keep class at.bitfire.** { *; } # all DAVx⁵ code is required
|
||||
|
||||
# AGP 8.2 and 8.3 seem to remove this class, but ezvcard.io uses it. See https://github.com/bitfireAT/davx5/issues/499
|
||||
-keep class javax.xml.namespace.QName { *; }
|
||||
|
||||
# we use enum classes (https://www.guardsquare.com/en/products/proguard/manual/examples#enumerations)
|
||||
-keepclassmembers,allowoptimization enum * {
|
||||
public static **[] values();
|
||||
public static ** valueOf(java.lang.String);
|
||||
}
|
||||
|
||||
# Additional rules which are now required since missing classes can't be ignored in R8 anymore.
|
||||
# [https://developer.android.com/build/releases/past-releases/agp-7-0-0-release-notes#r8-missing-class-warning]
|
||||
-dontwarn com.android.org.conscrypt.SSLParametersImpl
|
||||
-dontwarn com.github.erosb.jsonsKema.** # ical4j
|
||||
-dontwarn com.sun.jna.** # dnsjava
|
||||
-dontwarn groovy.**
|
||||
-dontwarn java.beans.Transient
|
||||
-dontwarn javax.cache.** # ical4j
|
||||
-dontwarn javax.naming.NamingException # dnsjava
|
||||
-dontwarn javax.naming.directory.** # dnsjava
|
||||
-dontwarn junit.textui.TestRunner
|
||||
-dontwarn lombok.** # dnsjava
|
||||
-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl
|
||||
-dontwarn org.bouncycastle.jsse.**
|
||||
-dontwarn org.codehaus.groovy.**
|
||||
-dontwarn org.joda.**
|
||||
-dontwarn org.jparsec.** # ical4j
|
||||
-dontwarn org.json.*
|
||||
-dontwarn org.jsoup.**
|
||||
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
|
||||
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
||||
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
||||
-dontwarn org.xmlpull.**
|
||||
-dontwarn sun.net.spi.nameservice.NameService
|
||||
-dontwarn sun.net.spi.nameservice.NameServiceDescriptor
|
||||
@@ -1,398 +0,0 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 10,
|
||||
"identityHash": "6fcabe50cbd00a4215dbe536a565dd2a",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "service",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountName",
|
||||
"columnName": "accountName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "principal",
|
||||
"columnName": "principal",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_service_accountName_type",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"accountName",
|
||||
"type"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "homeset",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceId",
|
||||
"columnName": "serviceId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "personal",
|
||||
"columnName": "personal",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "privBind",
|
||||
"columnName": "privBind",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_homeset_serviceId_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"serviceId",
|
||||
"url"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "service",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"serviceId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "collection",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `owner` TEXT, `color` INTEGER, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceId",
|
||||
"columnName": "serviceId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "homeSetId",
|
||||
"columnName": "homeSetId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "privWriteContent",
|
||||
"columnName": "privWriteContent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "privUnbind",
|
||||
"columnName": "privUnbind",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "forceReadOnly",
|
||||
"columnName": "forceReadOnly",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "owner",
|
||||
"columnName": "owner",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "timezone",
|
||||
"columnName": "timezone",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "supportsVEVENT",
|
||||
"columnName": "supportsVEVENT",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "supportsVTODO",
|
||||
"columnName": "supportsVTODO",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "supportsVJOURNAL",
|
||||
"columnName": "supportsVJOURNAL",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "source",
|
||||
"columnName": "source",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sync",
|
||||
"columnName": "sync",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_collection_serviceId_type",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"serviceId",
|
||||
"type"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)"
|
||||
},
|
||||
{
|
||||
"name": "index_collection_homeSetId_type",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"homeSetId",
|
||||
"type"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)"
|
||||
},
|
||||
{
|
||||
"name": "index_collection_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"url"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "service",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"serviceId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "homeset",
|
||||
"onDelete": "SET NULL",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"homeSetId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "syncstats",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "collectionId",
|
||||
"columnName": "collectionId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "authority",
|
||||
"columnName": "authority",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSync",
|
||||
"columnName": "lastSync",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_syncstats_collectionId_authority",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"collectionId",
|
||||
"authority"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "collection",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"collectionId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "webdav_mount",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6fcabe50cbd00a4215dbe536a565dd2a')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,536 +0,0 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 11,
|
||||
"identityHash": "223aa7f0fd53730921ca212a663585d8",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "service",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountName",
|
||||
"columnName": "accountName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "principal",
|
||||
"columnName": "principal",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_service_accountName_type",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"accountName",
|
||||
"type"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "homeset",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceId",
|
||||
"columnName": "serviceId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "personal",
|
||||
"columnName": "personal",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "privBind",
|
||||
"columnName": "privBind",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_homeset_serviceId_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"serviceId",
|
||||
"url"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "service",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"serviceId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "collection",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `owner` TEXT, `color` INTEGER, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceId",
|
||||
"columnName": "serviceId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "homeSetId",
|
||||
"columnName": "homeSetId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "privWriteContent",
|
||||
"columnName": "privWriteContent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "privUnbind",
|
||||
"columnName": "privUnbind",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "forceReadOnly",
|
||||
"columnName": "forceReadOnly",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "owner",
|
||||
"columnName": "owner",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "timezone",
|
||||
"columnName": "timezone",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "supportsVEVENT",
|
||||
"columnName": "supportsVEVENT",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "supportsVTODO",
|
||||
"columnName": "supportsVTODO",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "supportsVJOURNAL",
|
||||
"columnName": "supportsVJOURNAL",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "source",
|
||||
"columnName": "source",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sync",
|
||||
"columnName": "sync",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_collection_serviceId_type",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"serviceId",
|
||||
"type"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)"
|
||||
},
|
||||
{
|
||||
"name": "index_collection_homeSetId_type",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"homeSetId",
|
||||
"type"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)"
|
||||
},
|
||||
{
|
||||
"name": "index_collection_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"url"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "service",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"serviceId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "homeset",
|
||||
"onDelete": "SET NULL",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"homeSetId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "syncstats",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "collectionId",
|
||||
"columnName": "collectionId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "authority",
|
||||
"columnName": "authority",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSync",
|
||||
"columnName": "lastSync",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_syncstats_collectionId_authority",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"collectionId",
|
||||
"authority"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "collection",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"collectionId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "webdav_document",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mountId` INTEGER NOT NULL, `parentId` INTEGER, `name` TEXT NOT NULL, `isDirectory` INTEGER NOT NULL, `displayName` TEXT, `mimeType` TEXT, `eTag` TEXT, `lastModified` INTEGER, `size` INTEGER, `mayBind` INTEGER, `mayUnbind` INTEGER, `mayWriteContent` INTEGER, `quotaAvailable` INTEGER, `quotaUsed` INTEGER, FOREIGN KEY(`mountId`) REFERENCES `webdav_mount`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`parentId`) REFERENCES `webdav_document`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mountId",
|
||||
"columnName": "mountId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "parentId",
|
||||
"columnName": "parentId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isDirectory",
|
||||
"columnName": "isDirectory",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mimeType",
|
||||
"columnName": "mimeType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "eTag",
|
||||
"columnName": "eTag",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastModified",
|
||||
"columnName": "lastModified",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "size",
|
||||
"columnName": "size",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mayBind",
|
||||
"columnName": "mayBind",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mayUnbind",
|
||||
"columnName": "mayUnbind",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mayWriteContent",
|
||||
"columnName": "mayWriteContent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "quotaAvailable",
|
||||
"columnName": "quotaAvailable",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "quotaUsed",
|
||||
"columnName": "quotaUsed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_webdav_document_mountId_parentId_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"mountId",
|
||||
"parentId",
|
||||
"name"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_webdav_document_mountId_parentId_name` ON `${TABLE_NAME}` (`mountId`, `parentId`, `name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "webdav_mount",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"mountId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "webdav_document",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"parentId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "webdav_mount",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '223aa7f0fd53730921ca212a663585d8')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,615 +0,0 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 12,
|
||||
"identityHash": "67fafceecee2d97cac6a62d46fa2c3e2",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "service",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountName",
|
||||
"columnName": "accountName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "principal",
|
||||
"columnName": "principal",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_service_accountName_type",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"accountName",
|
||||
"type"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "homeset",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceId",
|
||||
"columnName": "serviceId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "personal",
|
||||
"columnName": "personal",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "privBind",
|
||||
"columnName": "privBind",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_homeset_serviceId_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"serviceId",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "service",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"serviceId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "collection",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `ownerId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `color` INTEGER, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`ownerId`) REFERENCES `principal`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceId",
|
||||
"columnName": "serviceId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "homeSetId",
|
||||
"columnName": "homeSetId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "ownerId",
|
||||
"columnName": "ownerId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "privWriteContent",
|
||||
"columnName": "privWriteContent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "privUnbind",
|
||||
"columnName": "privUnbind",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "forceReadOnly",
|
||||
"columnName": "forceReadOnly",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "timezone",
|
||||
"columnName": "timezone",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "supportsVEVENT",
|
||||
"columnName": "supportsVEVENT",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "supportsVTODO",
|
||||
"columnName": "supportsVTODO",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "supportsVJOURNAL",
|
||||
"columnName": "supportsVJOURNAL",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "source",
|
||||
"columnName": "source",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sync",
|
||||
"columnName": "sync",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_collection_serviceId_type",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"serviceId",
|
||||
"type"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)"
|
||||
},
|
||||
{
|
||||
"name": "index_collection_homeSetId_type",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"homeSetId",
|
||||
"type"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)"
|
||||
},
|
||||
{
|
||||
"name": "index_collection_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "service",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"serviceId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "homeset",
|
||||
"onDelete": "SET NULL",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"homeSetId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "principal",
|
||||
"onDelete": "SET NULL",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"ownerId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "principal",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `url` TEXT NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceId",
|
||||
"columnName": "serviceId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_principal_serviceId_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"serviceId",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_principal_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "service",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"serviceId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "syncstats",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "collectionId",
|
||||
"columnName": "collectionId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "authority",
|
||||
"columnName": "authority",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSync",
|
||||
"columnName": "lastSync",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_syncstats_collectionId_authority",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"collectionId",
|
||||
"authority"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "collection",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"collectionId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "webdav_document",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mountId` INTEGER NOT NULL, `parentId` INTEGER, `name` TEXT NOT NULL, `isDirectory` INTEGER NOT NULL, `displayName` TEXT, `mimeType` TEXT, `eTag` TEXT, `lastModified` INTEGER, `size` INTEGER, `mayBind` INTEGER, `mayUnbind` INTEGER, `mayWriteContent` INTEGER, `quotaAvailable` INTEGER, `quotaUsed` INTEGER, FOREIGN KEY(`mountId`) REFERENCES `webdav_mount`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`parentId`) REFERENCES `webdav_document`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mountId",
|
||||
"columnName": "mountId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "parentId",
|
||||
"columnName": "parentId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isDirectory",
|
||||
"columnName": "isDirectory",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mimeType",
|
||||
"columnName": "mimeType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "eTag",
|
||||
"columnName": "eTag",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastModified",
|
||||
"columnName": "lastModified",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "size",
|
||||
"columnName": "size",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mayBind",
|
||||
"columnName": "mayBind",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mayUnbind",
|
||||
"columnName": "mayUnbind",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mayWriteContent",
|
||||
"columnName": "mayWriteContent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "quotaAvailable",
|
||||
"columnName": "quotaAvailable",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "quotaUsed",
|
||||
"columnName": "quotaUsed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_webdav_document_mountId_parentId_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"mountId",
|
||||
"parentId",
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_webdav_document_mountId_parentId_name` ON `${TABLE_NAME}` (`mountId`, `parentId`, `name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "webdav_mount",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"mountId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "webdav_document",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"parentId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "webdav_mount",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"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, '67fafceecee2d97cac6a62d46fa2c3e2')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,640 +0,0 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 13,
|
||||
"identityHash": "0a6a9705ff471acd766ab96e3edf8ac3",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "service",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountName",
|
||||
"columnName": "accountName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "principal",
|
||||
"columnName": "principal",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_service_accountName_type",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"accountName",
|
||||
"type"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "homeset",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceId",
|
||||
"columnName": "serviceId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "personal",
|
||||
"columnName": "personal",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "privBind",
|
||||
"columnName": "privBind",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_homeset_serviceId_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"serviceId",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "service",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"serviceId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "collection",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `ownerId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `color` INTEGER, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, `pushTopic` TEXT, `supportsWebPush` INTEGER NOT NULL DEFAULT 0, `pushSubscription` TEXT, `pushSubscriptionCreated` INTEGER, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`ownerId`) REFERENCES `principal`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceId",
|
||||
"columnName": "serviceId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "homeSetId",
|
||||
"columnName": "homeSetId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "ownerId",
|
||||
"columnName": "ownerId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "privWriteContent",
|
||||
"columnName": "privWriteContent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "privUnbind",
|
||||
"columnName": "privUnbind",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "forceReadOnly",
|
||||
"columnName": "forceReadOnly",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "timezone",
|
||||
"columnName": "timezone",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "supportsVEVENT",
|
||||
"columnName": "supportsVEVENT",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "supportsVTODO",
|
||||
"columnName": "supportsVTODO",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "supportsVJOURNAL",
|
||||
"columnName": "supportsVJOURNAL",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "source",
|
||||
"columnName": "source",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sync",
|
||||
"columnName": "sync",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pushTopic",
|
||||
"columnName": "pushTopic",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "supportsWebPush",
|
||||
"columnName": "supportsWebPush",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pushSubscription",
|
||||
"columnName": "pushSubscription",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pushSubscriptionCreated",
|
||||
"columnName": "pushSubscriptionCreated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_collection_serviceId_type",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"serviceId",
|
||||
"type"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)"
|
||||
},
|
||||
{
|
||||
"name": "index_collection_homeSetId_type",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"homeSetId",
|
||||
"type"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)"
|
||||
},
|
||||
{
|
||||
"name": "index_collection_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "service",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"serviceId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "homeset",
|
||||
"onDelete": "SET NULL",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"homeSetId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "principal",
|
||||
"onDelete": "SET NULL",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"ownerId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "principal",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `url` TEXT NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceId",
|
||||
"columnName": "serviceId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_principal_serviceId_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"serviceId",
|
||||
"url"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_principal_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "service",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"serviceId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "syncstats",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "collectionId",
|
||||
"columnName": "collectionId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "authority",
|
||||
"columnName": "authority",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSync",
|
||||
"columnName": "lastSync",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_syncstats_collectionId_authority",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"collectionId",
|
||||
"authority"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "collection",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"collectionId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "webdav_document",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mountId` INTEGER NOT NULL, `parentId` INTEGER, `name` TEXT NOT NULL, `isDirectory` INTEGER NOT NULL, `displayName` TEXT, `mimeType` TEXT, `eTag` TEXT, `lastModified` INTEGER, `size` INTEGER, `mayBind` INTEGER, `mayUnbind` INTEGER, `mayWriteContent` INTEGER, `quotaAvailable` INTEGER, `quotaUsed` INTEGER, FOREIGN KEY(`mountId`) REFERENCES `webdav_mount`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`parentId`) REFERENCES `webdav_document`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mountId",
|
||||
"columnName": "mountId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "parentId",
|
||||
"columnName": "parentId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isDirectory",
|
||||
"columnName": "isDirectory",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mimeType",
|
||||
"columnName": "mimeType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "eTag",
|
||||
"columnName": "eTag",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastModified",
|
||||
"columnName": "lastModified",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "size",
|
||||
"columnName": "size",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mayBind",
|
||||
"columnName": "mayBind",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mayUnbind",
|
||||
"columnName": "mayUnbind",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "mayWriteContent",
|
||||
"columnName": "mayWriteContent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "quotaAvailable",
|
||||
"columnName": "quotaAvailable",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "quotaUsed",
|
||||
"columnName": "quotaUsed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_webdav_document_mountId_parentId_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"mountId",
|
||||
"parentId",
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_webdav_document_mountId_parentId_name` ON `${TABLE_NAME}` (`mountId`, `parentId`, `name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "webdav_mount",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"mountId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "webdav_document",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"parentId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "webdav_mount",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"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, '0a6a9705ff471acd766ab96e3edf8ac3')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 8,
|
||||
"identityHash": "b8699ef3cc4c62e8851df4360fb69e00",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "service",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountName",
|
||||
"columnName": "accountName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "principal",
|
||||
"columnName": "principal",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_service_accountName_type",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"accountName",
|
||||
"type"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "homeset",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceId",
|
||||
"columnName": "serviceId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "personal",
|
||||
"columnName": "personal",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "privBind",
|
||||
"columnName": "privBind",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_homeset_serviceId_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"serviceId",
|
||||
"url"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "service",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"serviceId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "collection",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `owner` TEXT, `color` INTEGER, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceId",
|
||||
"columnName": "serviceId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "homeSetId",
|
||||
"columnName": "homeSetId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "privWriteContent",
|
||||
"columnName": "privWriteContent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "privUnbind",
|
||||
"columnName": "privUnbind",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "forceReadOnly",
|
||||
"columnName": "forceReadOnly",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "owner",
|
||||
"columnName": "owner",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "timezone",
|
||||
"columnName": "timezone",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "supportsVEVENT",
|
||||
"columnName": "supportsVEVENT",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "supportsVTODO",
|
||||
"columnName": "supportsVTODO",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "supportsVJOURNAL",
|
||||
"columnName": "supportsVJOURNAL",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "source",
|
||||
"columnName": "source",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sync",
|
||||
"columnName": "sync",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_collection_serviceId_type",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"serviceId",
|
||||
"type"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)"
|
||||
},
|
||||
{
|
||||
"name": "index_collection_homeSetId_type",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"homeSetId",
|
||||
"type"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "service",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"serviceId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "homeset",
|
||||
"onDelete": "SET NULL",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"homeSetId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"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, 'b8699ef3cc4c62e8851df4360fb69e00')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 9,
|
||||
"identityHash": "7e4bfdf7f9fa3529c333cf9485f8cf50",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "service",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT NOT NULL, `type` TEXT NOT NULL, `principal` TEXT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountName",
|
||||
"columnName": "accountName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "principal",
|
||||
"columnName": "principal",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_service_accountName_type",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"accountName",
|
||||
"type"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_service_accountName_type` ON `${TABLE_NAME}` (`accountName`, `type`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "homeset",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `personal` INTEGER NOT NULL, `url` TEXT NOT NULL, `privBind` INTEGER NOT NULL, `displayName` TEXT, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceId",
|
||||
"columnName": "serviceId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "personal",
|
||||
"columnName": "personal",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "privBind",
|
||||
"columnName": "privBind",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_homeset_serviceId_url",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"serviceId",
|
||||
"url"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_homeset_serviceId_url` ON `${TABLE_NAME}` (`serviceId`, `url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "service",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"serviceId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "collection",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `serviceId` INTEGER NOT NULL, `homeSetId` INTEGER, `type` TEXT NOT NULL, `url` TEXT NOT NULL, `privWriteContent` INTEGER NOT NULL, `privUnbind` INTEGER NOT NULL, `forceReadOnly` INTEGER NOT NULL, `displayName` TEXT, `description` TEXT, `owner` TEXT, `color` INTEGER, `timezone` TEXT, `supportsVEVENT` INTEGER, `supportsVTODO` INTEGER, `supportsVJOURNAL` INTEGER, `source` TEXT, `sync` INTEGER NOT NULL, FOREIGN KEY(`serviceId`) REFERENCES `service`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`homeSetId`) REFERENCES `homeset`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "serviceId",
|
||||
"columnName": "serviceId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "homeSetId",
|
||||
"columnName": "homeSetId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "privWriteContent",
|
||||
"columnName": "privWriteContent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "privUnbind",
|
||||
"columnName": "privUnbind",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "forceReadOnly",
|
||||
"columnName": "forceReadOnly",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "owner",
|
||||
"columnName": "owner",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "timezone",
|
||||
"columnName": "timezone",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "supportsVEVENT",
|
||||
"columnName": "supportsVEVENT",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "supportsVTODO",
|
||||
"columnName": "supportsVTODO",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "supportsVJOURNAL",
|
||||
"columnName": "supportsVJOURNAL",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "source",
|
||||
"columnName": "source",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "sync",
|
||||
"columnName": "sync",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_collection_serviceId_type",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"serviceId",
|
||||
"type"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_serviceId_type` ON `${TABLE_NAME}` (`serviceId`, `type`)"
|
||||
},
|
||||
{
|
||||
"name": "index_collection_homeSetId_type",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"homeSetId",
|
||||
"type"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_homeSetId_type` ON `${TABLE_NAME}` (`homeSetId`, `type`)"
|
||||
},
|
||||
{
|
||||
"name": "index_collection_url",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"url"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_collection_url` ON `${TABLE_NAME}` (`url`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "service",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"serviceId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "homeset",
|
||||
"onDelete": "SET NULL",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"homeSetId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "syncstats",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `collectionId` INTEGER NOT NULL, `authority` TEXT NOT NULL, `lastSync` INTEGER NOT NULL, FOREIGN KEY(`collectionId`) REFERENCES `collection`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "collectionId",
|
||||
"columnName": "collectionId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "authority",
|
||||
"columnName": "authority",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSync",
|
||||
"columnName": "lastSync",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_syncstats_collectionId_authority",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"collectionId",
|
||||
"authority"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_syncstats_collectionId_authority` ON `${TABLE_NAME}` (`collectionId`, `authority`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "collection",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"collectionId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"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, '7e4bfdf7f9fa3529c333cf9485f8cf50')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
espressoTest
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:installLocation="internalOnly">
|
||||
|
||||
<!-- account management permissions not required for own accounts since API level 22 -->
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" android:maxSdkVersion="22"/>
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" android:maxSdkVersion="22"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" android:maxSdkVersion="22"/>
|
||||
|
||||
</manifest>
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.runner.AndroidJUnitRunner
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import dagger.hilt.android.testing.HiltTestApplication
|
||||
|
||||
class CustomTestRunner : AndroidJUnitRunner() {
|
||||
|
||||
override fun newApplication(cl: ClassLoader, name: String, context: Context) =
|
||||
super.newApplication(cl, HiltTestApplication::class.java.name, context)
|
||||
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import android.util.Xml
|
||||
import at.bitfire.dav4jvm.XmlUtils
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class Dav4jvm {
|
||||
|
||||
@Test
|
||||
fun test_Dav4jvm_XmlUtils_NewPullParser_RelaxedParsing() {
|
||||
val parser = XmlUtils.newPullParser()
|
||||
assertTrue(parser.getFeature(Xml.FEATURE_RELAXED))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import android.Manifest
|
||||
import android.accounts.Account
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.os.Build
|
||||
import android.provider.CalendarContract
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.resource.LocalCalendar
|
||||
import at.bitfire.davdroid.resource.LocalEvent
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import at.bitfire.ical4android.Event
|
||||
import net.fortuna.ical4j.model.property.DtStart
|
||||
import net.fortuna.ical4j.model.property.RRule
|
||||
import org.junit.rules.RuleChain
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
|
||||
/**
|
||||
* JUnit ClassRule which initializes the AOSP CalendarProvider.
|
||||
* Needed for some "flaky" tests which would otherwise only succeed on second run.
|
||||
*
|
||||
* Currently tested on development machine (Ryzen) with Android 12 images (with/without Google Play).
|
||||
* Calendar provider behaves quite randomly, so it may or may not work. If you (the reader
|
||||
* if this comment) can find out on how to initialize the calendar provider so that the
|
||||
* tests are reliably run after `adb shell pm clear com.android.providers.calendar`,
|
||||
* please let us know!
|
||||
*
|
||||
* If you run tests manually, just make sure to ignore the first run after the calendar
|
||||
* provider has been accessed the first time.
|
||||
*/
|
||||
class InitCalendarProviderRule private constructor(): TestRule {
|
||||
|
||||
companion object {
|
||||
fun getInstance() = RuleChain
|
||||
.outerRule(GrantPermissionRule.grant(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR))
|
||||
.around(InitCalendarProviderRule())
|
||||
}
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement {
|
||||
Logger.log.info("Initializing calendar provider before running ${description.displayName}")
|
||||
return InitCalendarProviderStatement(base)
|
||||
}
|
||||
|
||||
|
||||
class InitCalendarProviderStatement(val base: Statement): Statement() {
|
||||
|
||||
override fun evaluate() {
|
||||
if (Build.VERSION.SDK_INT < 31)
|
||||
Logger.log.warning("Calendar provider initialization may or may not work. See InitCalendarProviderRule")
|
||||
initCalendarProvider()
|
||||
|
||||
base.evaluate()
|
||||
}
|
||||
|
||||
private fun initCalendarProvider() {
|
||||
val account = Account("LocalCalendarTest", CalendarContract.ACCOUNT_TYPE_LOCAL)
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
val provider = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!!
|
||||
val uri = AndroidCalendar.create(account, provider, ContentValues())
|
||||
val calendar = AndroidCalendar.findByID(account, provider, LocalCalendar.Factory, ContentUris.parseId(uri))
|
||||
try {
|
||||
// single event init
|
||||
val normalEvent = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event with 1 instance"
|
||||
}
|
||||
val normalLocalEvent = LocalEvent(calendar, normalEvent, null, null, null, 0)
|
||||
normalLocalEvent.add()
|
||||
LocalEvent.numInstances(provider, account, normalLocalEvent.id!!)
|
||||
|
||||
// recurring event init
|
||||
val recurringEvent = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event over 22 years"
|
||||
rRules.add(RRule("FREQ=YEARLY;UNTIL=20740119T010203Z")) // year needs to be >2074 (not supported by Android <11 Calendar Storage)
|
||||
}
|
||||
val localRecurringEvent = LocalEvent(calendar, recurringEvent, null, null, null, 0)
|
||||
localRecurringEvent.add()
|
||||
LocalEvent.numInstances(provider, account, localRecurringEvent.id!!)
|
||||
} finally {
|
||||
calendar.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import android.content.Context
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.hilt.testing.TestInstallIn
|
||||
import io.mockk.spyk
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@TestInstallIn(
|
||||
components = [ SingletonComponent::class ],
|
||||
replaces = [
|
||||
SettingsManager.SettingsManagerModule::class
|
||||
]
|
||||
)
|
||||
class MockingModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun spykSettingsManager(@ApplicationContext context: Context): SettingsManager =
|
||||
spyk(SettingsManager(context))
|
||||
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkQuery
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
import org.junit.Assert.assertTrue
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.TimeoutException
|
||||
import kotlin.math.abs
|
||||
|
||||
object TestUtils {
|
||||
|
||||
fun assertWithin(expected: Long, actual: Long, tolerance: Long) {
|
||||
val absDifference = abs(expected - actual)
|
||||
assertTrue(
|
||||
"$actual not within ($expected ± $tolerance)",
|
||||
absDifference <= tolerance
|
||||
)
|
||||
}
|
||||
|
||||
@TestOnly
|
||||
fun workScheduledOrRunning(context: Context, workerName: String): Boolean =
|
||||
workInStates(context, workerName, listOf(
|
||||
WorkInfo.State.ENQUEUED,
|
||||
WorkInfo.State.RUNNING
|
||||
))
|
||||
|
||||
@TestOnly
|
||||
fun workScheduledOrRunningOrSuccessful(context: Context, workerName: String): Boolean =
|
||||
workInStates(context, workerName, listOf(
|
||||
WorkInfo.State.ENQUEUED,
|
||||
WorkInfo.State.RUNNING,
|
||||
WorkInfo.State.SUCCEEDED
|
||||
))
|
||||
|
||||
@TestOnly
|
||||
fun workInStates(context: Context, workerName: String, states: List<WorkInfo.State>): Boolean =
|
||||
WorkManager.getInstance(context).getWorkInfos(WorkQuery.Builder
|
||||
.fromUniqueWorkNames(listOf(workerName))
|
||||
.addStates(states)
|
||||
.build()
|
||||
).get().isNotEmpty()
|
||||
|
||||
|
||||
/* Copyright 2019 Google LLC.
|
||||
SPDX-License-Identifier: Apache-2.0 */
|
||||
@TestOnly
|
||||
fun <T> LiveData<T>.getOrAwaitValue(
|
||||
time: Long = 2,
|
||||
timeUnit: TimeUnit = TimeUnit.SECONDS
|
||||
): T {
|
||||
var data: T? = null
|
||||
val latch = CountDownLatch(1)
|
||||
val observer = object : Observer<T> {
|
||||
override fun onChanged(value: T) {
|
||||
data = value
|
||||
latch.countDown()
|
||||
this@getOrAwaitValue.removeObserver(this)
|
||||
}
|
||||
}
|
||||
|
||||
this.observeForever(observer)
|
||||
|
||||
// Don't wait indefinitely if the LiveData is not set.
|
||||
if (!latch.await(time, timeUnit)) {
|
||||
throw TimeoutException("LiveData value was never set.")
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return data as T
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class AppDatabaseTest {
|
||||
|
||||
val TEST_DB = "test"
|
||||
|
||||
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val helper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java,
|
||||
listOf(), // no auto migrations until v8
|
||||
FrameworkSQLiteOpenHelperFactory()
|
||||
)
|
||||
|
||||
|
||||
@Test
|
||||
fun testAllMigrations() {
|
||||
// DB schema is available since version 8, so create DB with v8
|
||||
helper.createDatabase(TEST_DB, 8).close()
|
||||
|
||||
val db = Room.databaseBuilder(context, AppDatabase::class.java, TEST_DB)
|
||||
// manual migrations
|
||||
.addMigrations(*AppDatabase.migrations)
|
||||
// auto-migrations that need to be specified explicitly
|
||||
.addAutoMigrationSpec(AppDatabase.AutoMigration11_12(context))
|
||||
.build()
|
||||
try {
|
||||
// open (with version 8) + migrate (to current version) database
|
||||
db.openHelper.writableDatabase
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import android.security.NetworkSecurityPolicy
|
||||
import androidx.test.filters.SmallTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.dav4jvm.DavResource
|
||||
import at.bitfire.dav4jvm.property.webdav.ResourceType
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assume
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
class CollectionTest {
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Inject
|
||||
lateinit var settingsManager: SettingsManager
|
||||
|
||||
@Before
|
||||
fun inject() {
|
||||
hiltRule.inject()
|
||||
}
|
||||
|
||||
|
||||
private lateinit var httpClient: HttpClient
|
||||
private val server = MockWebServer()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
httpClient = HttpClient.Builder(InstrumentationRegistry.getInstrumentation().targetContext).build()
|
||||
Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
|
||||
}
|
||||
|
||||
@After
|
||||
fun shutDown() {
|
||||
httpClient.close()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun testFromDavResponseAddressBook() {
|
||||
// r/w address book
|
||||
server.enqueue(MockResponse()
|
||||
.setResponseCode(207)
|
||||
.setBody("<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav'>" +
|
||||
"<response>" +
|
||||
" <href>/</href>" +
|
||||
" <propstat><prop>" +
|
||||
" <resourcetype><collection/><CARD:addressbook/></resourcetype>" +
|
||||
" <displayname>My Contacts</displayname>" +
|
||||
" <CARD:addressbook-description>My Contacts Description</CARD:addressbook-description>" +
|
||||
" </prop></propstat>" +
|
||||
"</response>" +
|
||||
"</multistatus>"))
|
||||
|
||||
lateinit var info: Collection
|
||||
DavResource(httpClient.okHttpClient, server.url("/"))
|
||||
.propfind(0, ResourceType.NAME) { response, _ ->
|
||||
info = Collection.fromDavResponse(response) ?: throw IllegalArgumentException()
|
||||
}
|
||||
assertEquals(Collection.TYPE_ADDRESSBOOK, info.type)
|
||||
assertTrue(info.privWriteContent)
|
||||
assertTrue(info.privUnbind)
|
||||
assertNull(info.supportsVEVENT)
|
||||
assertNull(info.supportsVTODO)
|
||||
assertNull(info.supportsVJOURNAL)
|
||||
assertEquals("My Contacts", info.displayName)
|
||||
assertEquals("My Contacts Description", info.description)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun testFromDavResponseCalendar() {
|
||||
// read-only calendar, no display name
|
||||
server.enqueue(MockResponse()
|
||||
.setResponseCode(207)
|
||||
.setBody("<multistatus xmlns='DAV:' xmlns:CAL='urn:ietf:params:xml:ns:caldav' xmlns:ICAL='http://apple.com/ns/ical/'>" +
|
||||
"<response>" +
|
||||
" <href>/</href>" +
|
||||
" <propstat><prop>" +
|
||||
" <resourcetype><collection/><CAL:calendar/></resourcetype>" +
|
||||
" <current-user-privilege-set><privilege><read/></privilege></current-user-privilege-set>" +
|
||||
" <CAL:calendar-description>My Calendar</CAL:calendar-description>" +
|
||||
" <CAL:calendar-timezone>tzdata</CAL:calendar-timezone>" +
|
||||
" <ICAL:calendar-color>#ff0000</ICAL:calendar-color>" +
|
||||
" </prop></propstat>" +
|
||||
"</response>" +
|
||||
"</multistatus>"))
|
||||
|
||||
lateinit var info: Collection
|
||||
DavResource(httpClient.okHttpClient, server.url("/"))
|
||||
.propfind(0, ResourceType.NAME) { response, _ ->
|
||||
info = Collection.fromDavResponse(response) ?: throw IllegalArgumentException()
|
||||
}
|
||||
assertEquals(Collection.TYPE_CALENDAR, info.type)
|
||||
assertFalse(info.privWriteContent)
|
||||
assertFalse(info.privUnbind)
|
||||
assertNull(info.displayName)
|
||||
assertEquals("My Calendar", info.description)
|
||||
assertEquals(0xFFFF0000.toInt(), info.color)
|
||||
assertEquals("tzdata", info.timezone)
|
||||
assertTrue(info.supportsVEVENT!!)
|
||||
assertTrue(info.supportsVTODO!!)
|
||||
assertTrue(info.supportsVJOURNAL!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun testFromDavResponseWebcal() {
|
||||
// Webcal subscription
|
||||
server.enqueue(MockResponse()
|
||||
.setResponseCode(207)
|
||||
.setBody("<multistatus xmlns='DAV:' xmlns:CS='http://calendarserver.org/ns/'>" +
|
||||
"<response>" +
|
||||
" <href>/webcal1</href>" +
|
||||
" <propstat><prop>" +
|
||||
" <displayname>Sample Subscription</displayname>" +
|
||||
" <resourcetype><collection/><CS:subscribed/></resourcetype>" +
|
||||
" <CS:source><href>webcals://example.com/1.ics</href></CS:source>" +
|
||||
" </prop></propstat>" +
|
||||
"</response>" +
|
||||
"</multistatus>"))
|
||||
|
||||
lateinit var info: Collection
|
||||
DavResource(httpClient.okHttpClient, server.url("/"))
|
||||
.propfind(0, ResourceType.NAME) { response, _ ->
|
||||
info = Collection.fromDavResponse(response) ?: throw IllegalArgumentException()
|
||||
}
|
||||
assertEquals(Collection.TYPE_WEBCAL, info.type)
|
||||
assertEquals("Sample Subscription", info.displayName)
|
||||
assertEquals("https://example.com/1.ics".toHttpUrl(), info.source)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
class HomesetDaoTest {
|
||||
|
||||
@get:Rule
|
||||
var hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
hiltRule.inject()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertOrUpdate() {
|
||||
// should insert new row or update (upsert) existing row - without changing its key!
|
||||
val serviceId = createTestService()
|
||||
val homeSetDao = db.homeSetDao()
|
||||
|
||||
val entry1 = HomeSet(id=0, serviceId=serviceId, personal=true, url="https://example.com/1".toHttpUrl())
|
||||
val insertId1 = homeSetDao.insertOrUpdateByUrl(entry1)
|
||||
assertEquals(1L, insertId1)
|
||||
assertEquals(entry1.apply { id = 1L }, homeSetDao.getById(1L))
|
||||
|
||||
val updatedEntry1 = HomeSet(id=0, serviceId=serviceId, personal=true, url="https://example.com/1".toHttpUrl(), displayName="Updated Entry")
|
||||
val updateId1 = homeSetDao.insertOrUpdateByUrl(updatedEntry1)
|
||||
assertEquals(1L, updateId1)
|
||||
assertEquals(updatedEntry1.apply { id = 1L }, homeSetDao.getById(1L))
|
||||
|
||||
val entry2 = HomeSet(id=0, serviceId=serviceId, personal=true, url= "https://example.com/2".toHttpUrl())
|
||||
val insertId2 = homeSetDao.insertOrUpdateByUrl(entry2)
|
||||
assertEquals(2L, insertId2)
|
||||
assertEquals(entry2.apply { id = 2L }, homeSetDao.getById(2L))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDelete() {
|
||||
// should delete row with given primary key (id)
|
||||
val serviceId = createTestService()
|
||||
val homesetDao = db.homeSetDao()
|
||||
|
||||
val entry1 = HomeSet(id=1, serviceId=serviceId, personal=true, url= "https://example.com/1".toHttpUrl())
|
||||
|
||||
val insertId1 = homesetDao.insertOrUpdateByUrl(entry1)
|
||||
assertEquals(1L, insertId1)
|
||||
assertEquals(entry1, homesetDao.getById(1L))
|
||||
|
||||
homesetDao.delete(entry1)
|
||||
|
||||
assertEquals(null, homesetDao.getById(1L))
|
||||
}
|
||||
|
||||
fun createTestService() : Long {
|
||||
val serviceDao = db.serviceDao()
|
||||
val service = Service(id=0, accountName="test", type=Service.TYPE_CALDAV, principal = null)
|
||||
return serviceDao.insertOrReplace(service)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.hilt.testing.TestInstallIn
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@TestInstallIn(
|
||||
components = [ SingletonComponent::class ],
|
||||
replaces = [
|
||||
AppDatabase.AppDatabaseModule::class
|
||||
]
|
||||
)
|
||||
class MemoryDbModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun inMemoryDatabase(@ApplicationContext context: Context): AppDatabase =
|
||||
Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
|
||||
// auto-migrations that need to be specified explicitly
|
||||
.addAutoMigrationSpec(AppDatabase.AutoMigration11_12(context))
|
||||
.build()
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.network
|
||||
|
||||
import android.os.Build
|
||||
import androidx.test.filters.SdkSuppress
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.xbill.DNS.ARecord
|
||||
import org.xbill.DNS.Lookup
|
||||
import org.xbill.DNS.Type
|
||||
import java.net.Inet4Address
|
||||
import java.net.InetAddress
|
||||
|
||||
class Android10ResolverTest {
|
||||
|
||||
val FQDN_DAVX5 = "www.davx5.com"
|
||||
|
||||
@Test
|
||||
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
|
||||
fun testResolveA() {
|
||||
val www = InetAddress.getAllByName(FQDN_DAVX5).filterIsInstance(Inet4Address::class.java).first()
|
||||
|
||||
val srvLookup = Lookup(FQDN_DAVX5, Type.A)
|
||||
srvLookup.setResolver(Android10Resolver)
|
||||
val resultGeneric = srvLookup.run()
|
||||
assertEquals(1, resultGeneric.size)
|
||||
|
||||
val result = resultGeneric.first() as ARecord
|
||||
assertEquals(www, result.address)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.network
|
||||
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
|
||||
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN
|
||||
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
|
||||
import android.net.NetworkCapabilities.TRANSPORT_WIFI
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.mockk.every
|
||||
import io.mockk.junit4.MockKRule
|
||||
import io.mockk.mockk
|
||||
import junit.framework.TestCase.assertFalse
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class ConnectionUtilsTest {
|
||||
|
||||
@get:Rule
|
||||
val mockkRule = MockKRule(this)
|
||||
|
||||
private val connectivityManager = mockk<ConnectivityManager>()
|
||||
private val network1 = mockk<Network>()
|
||||
private val network2 = mockk<Network>()
|
||||
private val capabilities = mockk<NetworkCapabilities>()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
every { connectivityManager.allNetworks } returns arrayOf(network1, network2)
|
||||
every { connectivityManager.getNetworkInfo(network1) } returns mockk()
|
||||
every { connectivityManager.getNetworkInfo(network2) } returns mockk()
|
||||
every { connectivityManager.getNetworkCapabilities(network1) } returns capabilities
|
||||
every { connectivityManager.getNetworkCapabilities(network2) } returns capabilities
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWifiAvailable_capabilitiesNull() {
|
||||
every { connectivityManager.getNetworkCapabilities(network1) } returns null
|
||||
every { connectivityManager.getNetworkCapabilities(network2) } returns null
|
||||
assertFalse(ConnectionUtils.wifiAvailable(connectivityManager))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWifiAvailable() {
|
||||
every { capabilities.hasTransport(TRANSPORT_WIFI) } returns false
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns false
|
||||
assertFalse(ConnectionUtils.wifiAvailable(connectivityManager))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWifiAvailable_wifi() {
|
||||
every { capabilities.hasTransport(TRANSPORT_WIFI) } returns true
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns false
|
||||
assertFalse(ConnectionUtils.wifiAvailable(connectivityManager))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWifiAvailable_validated() {
|
||||
every { capabilities.hasTransport(TRANSPORT_WIFI) } returns false
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns true
|
||||
assertFalse(ConnectionUtils.wifiAvailable(connectivityManager))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWifiAvailable_wifiValidated() {
|
||||
every { capabilities.hasTransport(TRANSPORT_WIFI) } returns true
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns true
|
||||
assertTrue(ConnectionUtils.wifiAvailable(connectivityManager))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testInternetAvailable_capabilitiesNull() {
|
||||
every { connectivityManager.getNetworkCapabilities(network1) } returns null
|
||||
every { connectivityManager.getNetworkCapabilities(network2) } returns null
|
||||
assertFalse(ConnectionUtils.internetAvailable(connectivityManager, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInternetAvailable_Internet() {
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_INTERNET) } returns true
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns false
|
||||
assertFalse(ConnectionUtils.internetAvailable(connectivityManager, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInternetAvailable_Validated() {
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_INTERNET) } returns false
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns true
|
||||
assertFalse(ConnectionUtils.internetAvailable(connectivityManager, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInternetAvailable_InternetValidated() {
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_INTERNET) } returns true
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns true
|
||||
assertTrue(ConnectionUtils.internetAvailable(connectivityManager, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInternetAvailable_ignoreVpns() {
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_INTERNET) } returns true
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns true
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_NOT_VPN) } returns false
|
||||
assertFalse(ConnectionUtils.internetAvailable(connectivityManager, true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInternetAvailable_ignoreVpns_Notvpn() {
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_INTERNET) } returns true
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns true
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_NOT_VPN) } returns true
|
||||
assertTrue(ConnectionUtils.internetAvailable(connectivityManager, true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInternetAvailable_twoConnectionsFirstOneWithoutInternet() {
|
||||
// The real case that failed in davx5-ose#395 is that the connection list contains (in this order)
|
||||
// 1. a mobile network without INTERNET, but with VALIDATED
|
||||
// 2. a WiFi network with INTERNET and VALIDATED
|
||||
|
||||
// The "return false" of hasINTERNET will trigger at the first connection, the
|
||||
// "andThen true" will trigger for the second connection
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_INTERNET) } returns false andThen true
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns true
|
||||
|
||||
// There is an internet connection if any(!) connection has both INTERNET and VALIDATED.
|
||||
assertTrue(ConnectionUtils.internetAvailable(connectivityManager, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInternetAvailable_twoConnectionsFirstOneWithoutValidated() {
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_INTERNET) } returns true
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns false andThen true
|
||||
assertTrue(ConnectionUtils.internetAvailable(connectivityManager, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInternetAvailable_twoConnectionsFirstOneWithoutNotvpn() {
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_INTERNET) } returns true
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_VALIDATED) } returns true
|
||||
every { capabilities.hasCapability(NET_CAPABILITY_NOT_VPN) } returns false andThen true
|
||||
assertTrue(ConnectionUtils.internetAvailable(connectivityManager, true))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.network
|
||||
|
||||
import android.security.NetworkSecurityPolicy
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import okhttp3.Request
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assume
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class HttpClientTest {
|
||||
|
||||
lateinit var server: MockWebServer
|
||||
lateinit var httpClient: HttpClient
|
||||
|
||||
@get:Rule
|
||||
var hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
hiltRule.inject()
|
||||
|
||||
httpClient = HttpClient.Builder(InstrumentationRegistry.getInstrumentation().targetContext).build()
|
||||
|
||||
server = MockWebServer()
|
||||
server.start(30000)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
server.shutdown()
|
||||
httpClient.close()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testCookies() {
|
||||
Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
|
||||
val url = server.url("/test")
|
||||
|
||||
// set cookie for root path (/) and /test path in first response
|
||||
server.enqueue(MockResponse()
|
||||
.setResponseCode(200)
|
||||
.addHeader("Set-Cookie", "cookie1=1; path=/")
|
||||
.addHeader("Set-Cookie", "cookie2=2")
|
||||
.setBody("Cookie set"))
|
||||
httpClient.okHttpClient.newCall(Request.Builder()
|
||||
.get().url(url)
|
||||
.build()).execute()
|
||||
assertNull(server.takeRequest().getHeader("Cookie"))
|
||||
|
||||
// cookie should be sent with second request
|
||||
// second response lets first cookie expire and overwrites second cookie
|
||||
server.enqueue(MockResponse()
|
||||
.addHeader("Set-Cookie", "cookie1=1a; path=/; Max-Age=0")
|
||||
.addHeader("Set-Cookie", "cookie2=2a")
|
||||
.setResponseCode(200))
|
||||
httpClient.okHttpClient.newCall(Request.Builder()
|
||||
.get().url(url)
|
||||
.build()).execute()
|
||||
assertEquals("cookie2=2; cookie1=1", server.takeRequest().getHeader("Cookie"))
|
||||
|
||||
server.enqueue(MockResponse()
|
||||
.setResponseCode(200))
|
||||
httpClient.okHttpClient.newCall(Request.Builder()
|
||||
.get().url(url)
|
||||
.build()).execute()
|
||||
assertEquals("cookie2=2a", server.takeRequest().getHeader("Cookie"))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.resource
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.davdroid.R
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class LocalAddressBookTest {
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
val mainAccountType = context.getString(R.string.account_type)
|
||||
val mainAccount = Account("main", mainAccountType)
|
||||
|
||||
val addressBookAccountType = context.getString(R.string.account_type_address_book)
|
||||
val addressBookAccount = Account("sub", addressBookAccountType)
|
||||
|
||||
val accountManager = AccountManager.get(context)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
hiltRule.inject()
|
||||
|
||||
// TODO DOES NOT WORK: the account immediately starts to sync, which creates the sync adapter services.
|
||||
// The services however can't be created because Hilt is "not ready" (although it has been initialized in the line above).
|
||||
// assertTrue(AccountUtils.createAccount(context, mainAccount, AccountSettings.initialUserData(null)))
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanup() {
|
||||
accountManager.removeAccount(addressBookAccount, null, null)
|
||||
accountManager.removeAccount(mainAccount, null, null)
|
||||
}
|
||||
|
||||
|
||||
// TODO see above
|
||||
/*@Test
|
||||
fun testMainAccount_AddressBookAccount_WithMainAccount() {
|
||||
// create address book account
|
||||
assertTrue(accountManager.addAccountExplicitly(addressBookAccount, null, Bundle().apply {
|
||||
putString(LocalAddressBook.USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name)
|
||||
putString(LocalAddressBook.USER_DATA_MAIN_ACCOUNT_TYPE, mainAccount.type)
|
||||
}))
|
||||
|
||||
// check mainAccount()
|
||||
assertEquals(mainAccount, LocalAddressBook.mainAccount(context, addressBookAccount))
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun testMainAccount_AddressBookAccount_WithoutMainAccount() {
|
||||
// create address book account
|
||||
assertTrue(accountManager.addAccountExplicitly(addressBookAccount, null, Bundle()))
|
||||
|
||||
// check mainAccount(); should fail because there's no main account
|
||||
LocalAddressBook.mainAccount(context, addressBookAccount)
|
||||
}*/
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun testMainAccount_OtherAccount() {
|
||||
LocalAddressBook.mainAccount(context, Account("Other Account", "com.example"))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.resource
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL
|
||||
import android.provider.CalendarContract.Events
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.davdroid.InitCalendarProviderRule
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import at.bitfire.ical4android.Event
|
||||
import at.bitfire.ical4android.util.MiscUtils.closeCompat
|
||||
import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter
|
||||
import net.fortuna.ical4j.model.property.DtStart
|
||||
import net.fortuna.ical4j.model.property.RRule
|
||||
import net.fortuna.ical4j.model.property.RecurrenceId
|
||||
import net.fortuna.ical4j.model.property.Status
|
||||
import org.junit.*
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.rules.TestRule
|
||||
|
||||
class LocalCalendarTest {
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
@ClassRule
|
||||
val initCalendarProviderRule: TestRule = InitCalendarProviderRule.getInstance()
|
||||
|
||||
private lateinit var provider: ContentProviderClient
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun setUpProvider() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
provider = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!!
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@JvmStatic
|
||||
fun closeProvider() {
|
||||
provider.closeCompat()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val account = Account("LocalCalendarTest", ACCOUNT_TYPE_LOCAL)
|
||||
private lateinit var calendar: LocalCalendar
|
||||
|
||||
@Before
|
||||
fun prepare() {
|
||||
val uri = AndroidCalendar.create(account, provider, ContentValues())
|
||||
calendar = AndroidCalendar.findByID(account, provider, LocalCalendar.Factory, ContentUris.parseId(uri))
|
||||
}
|
||||
|
||||
@After
|
||||
fun shutdown() {
|
||||
calendar.delete()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testDeleteDirtyEventsWithoutInstances_NoInstances_CancelledExceptions() {
|
||||
// create recurring event with only deleted/cancelled instances
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event with 3 instances"
|
||||
rRules.add(RRule("FREQ=DAILY;COUNT=3"))
|
||||
exceptions.add(Event().apply {
|
||||
recurrenceId = RecurrenceId("20220120T010203Z")
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Cancelled exception on 1st day"
|
||||
status = Status.VEVENT_CANCELLED
|
||||
})
|
||||
exceptions.add(Event().apply {
|
||||
recurrenceId = RecurrenceId("20220121T010203Z")
|
||||
dtStart = DtStart("20220121T010203Z")
|
||||
summary = "Cancelled exception on 2nd day"
|
||||
status = Status.VEVENT_CANCELLED
|
||||
})
|
||||
exceptions.add(Event().apply {
|
||||
recurrenceId = RecurrenceId("20220122T010203Z")
|
||||
dtStart = DtStart("20220122T010203Z")
|
||||
summary = "Cancelled exception on 3rd day"
|
||||
status = Status.VEVENT_CANCELLED
|
||||
})
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, "filename.ics", null, null, LocalResource.FLAG_REMOTELY_PRESENT)
|
||||
localEvent.add()
|
||||
val eventId = localEvent.id!!
|
||||
|
||||
// set event as dirty
|
||||
provider.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply {
|
||||
put(Events.DIRTY, 1)
|
||||
}, null, null)
|
||||
|
||||
// this method should mark the event as deleted
|
||||
calendar.deleteDirtyEventsWithoutInstances()
|
||||
|
||||
// verify that event is now marked as deleted
|
||||
provider.query(
|
||||
ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId),
|
||||
arrayOf(Events.DELETED), null, null, null
|
||||
)!!.use { cursor ->
|
||||
cursor.moveToNext()
|
||||
assertEquals(1, cursor.getInt(0))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
// Flaky, Needs single or rec init of CalendarProvider (InitCalendarProviderRule)
|
||||
fun testDeleteDirtyEventsWithoutInstances_Recurring_Instances() {
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event with 3 instances"
|
||||
rRules.add(RRule("FREQ=DAILY;COUNT=3"))
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, "filename.ics", null, null, LocalResource.FLAG_REMOTELY_PRESENT)
|
||||
localEvent.add()
|
||||
val eventId = localEvent.id!!
|
||||
|
||||
// set event as dirty
|
||||
provider.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply {
|
||||
put(Events.DIRTY, 1)
|
||||
}, null, null)
|
||||
|
||||
// this method should mark the event as deleted
|
||||
calendar.deleteDirtyEventsWithoutInstances()
|
||||
|
||||
// verify that event is not marked as deleted
|
||||
provider.query(
|
||||
ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId),
|
||||
arrayOf(Events.DELETED), null, null, null
|
||||
)!!.use { cursor ->
|
||||
cursor.moveToNext()
|
||||
assertEquals(0, cursor.getInt(0))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,472 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.resource
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.os.Build
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL
|
||||
import android.provider.CalendarContract.Events
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.davdroid.InitCalendarProviderRule
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import at.bitfire.ical4android.Event
|
||||
import at.bitfire.ical4android.util.MiscUtils.closeCompat
|
||||
import at.techbee.jtx.JtxContract.asSyncAdapter
|
||||
import net.fortuna.ical4j.model.Date
|
||||
import net.fortuna.ical4j.model.DateList
|
||||
import net.fortuna.ical4j.model.parameter.Value
|
||||
import net.fortuna.ical4j.model.property.*
|
||||
import org.junit.*
|
||||
import org.junit.Assert.*
|
||||
import org.junit.rules.TestRule
|
||||
import java.util.*
|
||||
|
||||
class LocalEventTest {
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmField
|
||||
@ClassRule
|
||||
val initCalendarProviderRule: TestRule = InitCalendarProviderRule.getInstance()
|
||||
|
||||
private val account = Account("LocalCalendarTest", ACCOUNT_TYPE_LOCAL)
|
||||
|
||||
private lateinit var provider: ContentProviderClient
|
||||
private lateinit var calendar: LocalCalendar
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun connect() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
provider = context.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!!
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@JvmStatic
|
||||
fun disconnect() {
|
||||
provider.closeCompat()
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
fun createCalendar() {
|
||||
val uri = AndroidCalendar.create(account, provider, ContentValues())
|
||||
calendar = AndroidCalendar.findByID(account, provider, LocalCalendar.Factory, ContentUris.parseId(uri))
|
||||
}
|
||||
|
||||
@After
|
||||
fun removeCalendar() {
|
||||
calendar.delete()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testNumDirectInstances_SingleInstance() {
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event with 1 instance"
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
|
||||
localEvent.add()
|
||||
|
||||
assertEquals(1, LocalEvent.numDirectInstances(provider, account, localEvent.id!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNumDirectInstances_Recurring() {
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event with 5 instances"
|
||||
rRules.add(RRule("FREQ=DAILY;COUNT=5"))
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
|
||||
localEvent.add()
|
||||
|
||||
assertEquals(5, LocalEvent.numDirectInstances(provider, account, localEvent.id!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNumDirectInstances_Recurring_Endless() {
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event without end"
|
||||
rRules.add(RRule("FREQ=DAILY"))
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
|
||||
localEvent.add()
|
||||
|
||||
assertNull(LocalEvent.numDirectInstances(provider, account, localEvent.id!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
// flaky, needs InitCalendarProviderRule
|
||||
fun testNumDirectInstances_Recurring_LateEnd() {
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event with 53 years"
|
||||
rRules.add(RRule("FREQ=YEARLY;UNTIL=20740119T010203Z")) // year 2074 is not supported by Android <11 Calendar Storage
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
|
||||
localEvent.add()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
|
||||
assertEquals(52, LocalEvent.numDirectInstances(provider, account, localEvent.id!!))
|
||||
else
|
||||
assertNull(LocalEvent.numDirectInstances(provider, account, localEvent.id!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
// flaky, needs InitCalendarProviderRule
|
||||
fun testNumDirectInstances_Recurring_ManyInstances() {
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event with 2 years"
|
||||
rRules.add(RRule("FREQ=DAILY;UNTIL=20240120T010203Z"))
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
|
||||
localEvent.add()
|
||||
val number = LocalEvent.numDirectInstances(provider, account, localEvent.id!!)
|
||||
|
||||
// Some android versions (i.e. <=Q and S) return 365*2 instances (wrong, 365*2+1 => correct),
|
||||
// but we are satisfied with either result for now
|
||||
assertTrue(number == 365*2 || number == 365*2+1)
|
||||
}
|
||||
|
||||
@Test
|
||||
// flaky, needs InitCalendarProviderRule
|
||||
fun testNumDirectInstances_RecurringWithExdate() {
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart(Date("20220120T010203Z"))
|
||||
summary = "Event with 5 instances"
|
||||
rRules.add(RRule("FREQ=DAILY;COUNT=5"))
|
||||
exDates.add(ExDate(DateList("20220121T010203Z", Value.DATE_TIME)))
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
|
||||
localEvent.add()
|
||||
|
||||
assertEquals(4, LocalEvent.numDirectInstances(provider, account, localEvent.id!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNumDirectInstances_RecurringWithExceptions() {
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event with 5 instances"
|
||||
rRules.add(RRule("FREQ=DAILY;COUNT=5"))
|
||||
exceptions.add(Event().apply {
|
||||
recurrenceId = RecurrenceId("20220122T010203Z")
|
||||
dtStart = DtStart("20220122T130203Z")
|
||||
summary = "Exception on 3rd day"
|
||||
})
|
||||
exceptions.add(Event().apply {
|
||||
recurrenceId = RecurrenceId("20220124T010203Z")
|
||||
dtStart = DtStart("20220122T160203Z")
|
||||
summary = "Exception on 5th day"
|
||||
})
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, "filename.ics", null, null, 0)
|
||||
localEvent.add()
|
||||
|
||||
assertEquals(5-2, LocalEvent.numDirectInstances(provider, account, localEvent.id!!))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
// flaky, needs InitCalendarProviderRule
|
||||
fun testNumInstances_SingleInstance() {
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event with 1 instance"
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
|
||||
localEvent.add()
|
||||
|
||||
assertEquals(1, LocalEvent.numInstances(provider, account, localEvent.id!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNumInstances_Recurring() {
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event with 5 instances"
|
||||
rRules.add(RRule("FREQ=DAILY;COUNT=5"))
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
|
||||
localEvent.add()
|
||||
|
||||
assertEquals(5, LocalEvent.numInstances(provider, account, localEvent.id!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNumInstances_Recurring_Endless() {
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event with infinite instances"
|
||||
rRules.add(RRule("FREQ=YEARLY"))
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
|
||||
localEvent.add()
|
||||
|
||||
assertNull(LocalEvent.numInstances(provider, account, localEvent.id!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
// flaky, needs InitCalendarProviderRule
|
||||
fun testNumInstances_Recurring_LateEnd() {
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event over 22 years"
|
||||
rRules.add(RRule("FREQ=YEARLY;UNTIL=20740119T010203Z")) // year 2074 not supported by Android <11 Calendar Storage
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
|
||||
localEvent.add()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
|
||||
assertEquals(52, LocalEvent.numInstances(provider, account, localEvent.id!!))
|
||||
else
|
||||
assertNull(LocalEvent.numInstances(provider, account, localEvent.id!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
// flaky, needs InitCalendarProviderRule
|
||||
fun testNumInstances_Recurring_ManyInstances() {
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event over two years"
|
||||
rRules.add(RRule("FREQ=DAILY;UNTIL=20240120T010203Z"))
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
|
||||
localEvent.add()
|
||||
|
||||
assertEquals(
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q)
|
||||
365*2 // Android <10: does not include UNTIL (incorrect!)
|
||||
else
|
||||
365*2 + 1, // Android ≥10: includes UNTIL (correct)
|
||||
LocalEvent.numInstances(provider, account, localEvent.id!!))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNumInstances_RecurringWithExceptions() {
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event with 6 instances"
|
||||
rRules.add(RRule("FREQ=DAILY;COUNT=6"))
|
||||
exceptions.add(Event().apply {
|
||||
recurrenceId = RecurrenceId("20220122T010203Z")
|
||||
dtStart = DtStart("20220122T130203Z")
|
||||
summary = "Exception on 3rd day"
|
||||
})
|
||||
exceptions.add(Event().apply {
|
||||
recurrenceId = RecurrenceId("20220124T010203Z")
|
||||
dtStart = DtStart("20220122T160203Z")
|
||||
summary = "Exception on 5th day"
|
||||
})
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, "filename.ics", null, null, 0)
|
||||
val uri = localEvent.add()
|
||||
|
||||
calendar.findById(localEvent.id!!)
|
||||
|
||||
assertEquals(6, LocalEvent.numInstances(provider, account, localEvent.id!!))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testMarkEventAsDeleted() {
|
||||
// Create event
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "A fine event"
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
|
||||
localEvent.add()
|
||||
|
||||
// Delete event
|
||||
LocalEvent.markAsDeleted(provider, account, localEvent.id!!)
|
||||
|
||||
// Get the status of whether the event is deleted
|
||||
provider.query(
|
||||
ContentUris.withAppendedId(Events.CONTENT_URI, localEvent.id!!).asSyncAdapter(account),
|
||||
arrayOf(Events.DELETED),
|
||||
null,
|
||||
null, null
|
||||
)!!.use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
assertEquals(1, cursor.getInt(0))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testPrepareForUpload_NoUid() {
|
||||
// create event
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event without uid"
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
|
||||
localEvent.add() // save it to calendar storage
|
||||
|
||||
// prepare for upload - this should generate a new random uuid, returned as filename
|
||||
val fileNameWithSuffix = localEvent.prepareForUpload()
|
||||
val fileName = fileNameWithSuffix.removeSuffix(".ics")
|
||||
|
||||
// throws an exception if fileName is not an UUID
|
||||
UUID.fromString(fileName)
|
||||
|
||||
// UID in calendar storage should be the same as file name
|
||||
provider.query(
|
||||
ContentUris.withAppendedId(Events.CONTENT_URI, localEvent.id!!).asSyncAdapter(account),
|
||||
arrayOf(Events.UID_2445), null, null, null
|
||||
)!!.use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
assertEquals(fileName, cursor.getString(0))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPrepareForUpload_NormalUid() {
|
||||
// create event
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event with normal uid"
|
||||
uid = "some-event@hostname.tld" // old UID format, UUID would be new format
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
|
||||
localEvent.add() // save it to calendar storage
|
||||
|
||||
// prepare for upload - this should use the UID for the file name
|
||||
val fileNameWithSuffix = localEvent.prepareForUpload()
|
||||
val fileName = fileNameWithSuffix.removeSuffix(".ics")
|
||||
|
||||
assertEquals(event.uid, fileName)
|
||||
|
||||
// UID in calendar storage should still be set, too
|
||||
provider.query(
|
||||
ContentUris.withAppendedId(Events.CONTENT_URI, localEvent.id!!).asSyncAdapter(account),
|
||||
arrayOf(Events.UID_2445), null, null, null
|
||||
)!!.use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
assertEquals(fileName, cursor.getString(0))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPrepareForUpload_UidHasDangerousChars() {
|
||||
// create event
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event with funny uid"
|
||||
uid = "https://www.example.com/events/asdfewfe-cxyb-ewrws-sadfrwerxyvser-asdfxye-"
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, null, null, null, 0)
|
||||
localEvent.add() // save it to calendar storage
|
||||
|
||||
// prepare for upload - this should generate a new random uuid, returned as filename
|
||||
val fileNameWithSuffix = localEvent.prepareForUpload()
|
||||
val fileName = fileNameWithSuffix.removeSuffix(".ics")
|
||||
|
||||
// throws an exception if fileName is not an UUID
|
||||
UUID.fromString(fileName)
|
||||
|
||||
// UID in calendar storage shouldn't have been changed
|
||||
provider.query(
|
||||
ContentUris.withAppendedId(Events.CONTENT_URI, localEvent.id!!).asSyncAdapter(account),
|
||||
arrayOf(Events.UID_2445), null, null, null
|
||||
)!!.use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
assertEquals(event.uid, cursor.getString(0))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testDeleteDirtyEventsWithoutInstances_NoInstances_Exdate() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeleteDirtyEventsWithoutInstances_NoInstances_CancelledExceptions() {
|
||||
// create recurring event with only deleted/cancelled instances
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event with 3 instances"
|
||||
rRules.add(RRule("FREQ=DAILY;COUNT=3"))
|
||||
exceptions.add(Event().apply {
|
||||
recurrenceId = RecurrenceId("20220120T010203Z")
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Cancelled exception on 1st day"
|
||||
status = Status.VEVENT_CANCELLED
|
||||
})
|
||||
exceptions.add(Event().apply {
|
||||
recurrenceId = RecurrenceId("20220121T010203Z")
|
||||
dtStart = DtStart("20220121T010203Z")
|
||||
summary = "Cancelled exception on 2nd day"
|
||||
status = Status.VEVENT_CANCELLED
|
||||
})
|
||||
exceptions.add(Event().apply {
|
||||
recurrenceId = RecurrenceId("20220122T010203Z")
|
||||
dtStart = DtStart("20220122T010203Z")
|
||||
summary = "Cancelled exception on 3rd day"
|
||||
status = Status.VEVENT_CANCELLED
|
||||
})
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, "filename.ics", null, null, LocalResource.FLAG_REMOTELY_PRESENT)
|
||||
localEvent.add()
|
||||
val eventId = localEvent.id!!
|
||||
|
||||
// set event as dirty
|
||||
provider.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply {
|
||||
put(Events.DIRTY, 1)
|
||||
}, null, null)
|
||||
|
||||
// this method should mark the event as deleted
|
||||
calendar.deleteDirtyEventsWithoutInstances()
|
||||
|
||||
// verify that event is now marked as deleted
|
||||
provider.query(
|
||||
ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId),
|
||||
arrayOf(Events.DELETED), null, null, null
|
||||
)!!.use { cursor ->
|
||||
cursor.moveToNext()
|
||||
assertEquals(1, cursor.getInt(0))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDeleteDirtyEventsWithoutInstances_Recurring_Instances() {
|
||||
val event = Event().apply {
|
||||
dtStart = DtStart("20220120T010203Z")
|
||||
summary = "Event with 3 instances"
|
||||
rRules.add(RRule("FREQ=DAILY;COUNT=3"))
|
||||
}
|
||||
val localEvent = LocalEvent(calendar, event, "filename.ics", null, null, LocalResource.FLAG_REMOTELY_PRESENT)
|
||||
localEvent.add()
|
||||
val eventId = localEvent.id!!
|
||||
|
||||
// set event as dirty
|
||||
provider.update(ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId), ContentValues(1).apply {
|
||||
put(Events.DIRTY, 1)
|
||||
}, null, null)
|
||||
|
||||
// this method should mark the event as deleted
|
||||
calendar.deleteDirtyEventsWithoutInstances()
|
||||
|
||||
// verify that event is not marked as deleted
|
||||
provider.query(
|
||||
ContentUris.withAppendedId(Events.CONTENT_URI.asSyncAdapter(account), eventId),
|
||||
arrayOf(Events.DELETED), null, null, null
|
||||
)!!.use { cursor ->
|
||||
cursor.moveToNext()
|
||||
assertEquals(0, cursor.getInt(0))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.resource
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.provider.ContactsContract
|
||||
import android.provider.ContactsContract.CommonDataKinds.GroupMembership
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import at.bitfire.vcard4android.BatchOperation
|
||||
import at.bitfire.vcard4android.CachedGroupMembership
|
||||
import at.bitfire.vcard4android.Contact
|
||||
import at.bitfire.vcard4android.GroupMethod
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import org.junit.*
|
||||
import org.junit.Assert.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
class LocalGroupTest {
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Inject
|
||||
lateinit var settingsManager: SettingsManager
|
||||
|
||||
@Before
|
||||
fun inject() {
|
||||
hiltRule.inject()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
@ClassRule
|
||||
val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!!
|
||||
|
||||
private lateinit var provider: ContentProviderClient
|
||||
|
||||
private lateinit var addressBookGroupsAsCategories: LocalTestAddressBook
|
||||
private lateinit var addressBookGroupsAsVCards: LocalTestAddressBook
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun connect() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!!
|
||||
assertNotNull(provider)
|
||||
|
||||
addressBookGroupsAsCategories = LocalTestAddressBook(context, provider, GroupMethod.CATEGORIES)
|
||||
addressBookGroupsAsVCards = LocalTestAddressBook(context, provider, GroupMethod.GROUP_VCARDS)
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@JvmStatic
|
||||
fun disconnect() {
|
||||
@Suppress("DEPRECATION")
|
||||
provider.release()
|
||||
}
|
||||
}
|
||||
|
||||
private fun newGroup(addressBook: LocalAddressBook = addressBookGroupsAsCategories): LocalGroup =
|
||||
LocalGroup(addressBook,
|
||||
Contact().apply {
|
||||
displayName = "Test Group"
|
||||
}, null, null, 0
|
||||
).apply {
|
||||
add()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Before
|
||||
fun clearContacts() {
|
||||
addressBookGroupsAsCategories.clear()
|
||||
addressBookGroupsAsVCards.clear()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testApplyPendingMemberships_addPendingMembership() {
|
||||
val ab = addressBookGroupsAsVCards
|
||||
|
||||
val contact1 = LocalContact(ab, Contact().apply {
|
||||
uid = "test1"
|
||||
displayName = "Test"
|
||||
}, "test1.vcf", null, 0)
|
||||
contact1.add()
|
||||
|
||||
val group = newGroup(ab)
|
||||
// set pending membership of contact1
|
||||
ab.provider!!.update(
|
||||
ContentUris.withAppendedId(ab.groupsSyncUri(), group.id!!),
|
||||
ContentValues().apply {
|
||||
put(LocalGroup.COLUMN_PENDING_MEMBERS, LocalGroup.PendingMemberships(setOf("test1")).toString())
|
||||
},
|
||||
null, null
|
||||
)
|
||||
|
||||
// pending membership -> contact1 should be added to group
|
||||
LocalGroup.applyPendingMemberships(ab)
|
||||
|
||||
// check group membership
|
||||
ab.provider!!.query(
|
||||
ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), arrayOf(GroupMembership.GROUP_ROW_ID, GroupMembership.RAW_CONTACT_ID),
|
||||
"${GroupMembership.MIMETYPE}=?", arrayOf(GroupMembership.CONTENT_ITEM_TYPE),
|
||||
null
|
||||
)!!.use { cursor ->
|
||||
assertTrue(cursor.moveToNext())
|
||||
assertEquals(group.id, cursor.getLong(0))
|
||||
assertEquals(contact1.id, cursor.getLong(1))
|
||||
|
||||
assertFalse(cursor.moveToNext())
|
||||
}
|
||||
// check cached group membership
|
||||
ab.provider!!.query(
|
||||
ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), arrayOf(CachedGroupMembership.GROUP_ID, CachedGroupMembership.RAW_CONTACT_ID),
|
||||
"${CachedGroupMembership.MIMETYPE}=?", arrayOf(CachedGroupMembership.CONTENT_ITEM_TYPE),
|
||||
null
|
||||
)!!.use { cursor ->
|
||||
assertTrue(cursor.moveToNext())
|
||||
assertEquals(group.id, cursor.getLong(0))
|
||||
assertEquals(contact1.id, cursor.getLong(1))
|
||||
|
||||
assertFalse(cursor.moveToNext())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testApplyPendingMemberships_removeMembership() {
|
||||
val ab = addressBookGroupsAsVCards
|
||||
|
||||
val contact1 = LocalContact(ab, Contact().apply {
|
||||
uid = "test1"
|
||||
displayName = "Test"
|
||||
}, "test1.vcf", null, 0)
|
||||
contact1.add()
|
||||
|
||||
val group = newGroup(ab)
|
||||
|
||||
// add contact1 to group
|
||||
val batch = BatchOperation(ab.provider!!)
|
||||
contact1.addToGroup(batch, group.id!!)
|
||||
batch.commit()
|
||||
|
||||
// no pending memberships -> membership should be removed
|
||||
LocalGroup.applyPendingMemberships(ab)
|
||||
|
||||
// check group membership
|
||||
ab.provider!!.query(
|
||||
ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), arrayOf(GroupMembership.GROUP_ROW_ID, GroupMembership.RAW_CONTACT_ID),
|
||||
"${GroupMembership.MIMETYPE}=?", arrayOf(GroupMembership.CONTENT_ITEM_TYPE),
|
||||
null
|
||||
)!!.use { cursor ->
|
||||
assertFalse(cursor.moveToNext())
|
||||
}
|
||||
// check cached group membership
|
||||
ab.provider!!.query(
|
||||
ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), arrayOf(CachedGroupMembership.GROUP_ID, CachedGroupMembership.RAW_CONTACT_ID),
|
||||
"${CachedGroupMembership.MIMETYPE}=?", arrayOf(CachedGroupMembership.CONTENT_ITEM_TYPE),
|
||||
null
|
||||
)!!.use { cursor ->
|
||||
assertFalse(cursor.moveToNext())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testClearDirty_addCachedGroupMembership() {
|
||||
val ab = addressBookGroupsAsCategories
|
||||
val group = newGroup(ab)
|
||||
|
||||
val contact1 = LocalContact(ab, Contact().apply { displayName = "Test" }, "fn.vcf", null, 0)
|
||||
contact1.add()
|
||||
|
||||
// insert group membership, but no cached group membership
|
||||
ab.provider!!.insert(
|
||||
ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), ContentValues().apply {
|
||||
put(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE)
|
||||
put(GroupMembership.RAW_CONTACT_ID, contact1.id)
|
||||
put(GroupMembership.GROUP_ROW_ID, group.id)
|
||||
}
|
||||
)
|
||||
|
||||
group.clearDirty(null, null)
|
||||
|
||||
// check cached group membership
|
||||
ab.provider!!.query(
|
||||
ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), arrayOf(CachedGroupMembership.GROUP_ID, CachedGroupMembership.RAW_CONTACT_ID),
|
||||
"${CachedGroupMembership.MIMETYPE}=?", arrayOf(CachedGroupMembership.CONTENT_ITEM_TYPE),
|
||||
null
|
||||
)!!.use { cursor ->
|
||||
assertTrue(cursor.moveToNext())
|
||||
assertEquals(group.id, cursor.getLong(0))
|
||||
assertEquals(contact1.id, cursor.getLong(1))
|
||||
|
||||
assertFalse(cursor.moveToNext())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClearDirty_removeCachedGroupMembership() {
|
||||
val ab = addressBookGroupsAsCategories
|
||||
val group = newGroup(ab)
|
||||
|
||||
val contact1 = LocalContact(ab, Contact().apply { displayName = "Test" }, "fn.vcf", null, 0)
|
||||
contact1.add()
|
||||
|
||||
// insert cached group membership, but no group membership
|
||||
ab.provider!!.insert(
|
||||
ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), ContentValues().apply {
|
||||
put(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
|
||||
put(CachedGroupMembership.RAW_CONTACT_ID, contact1.id)
|
||||
put(CachedGroupMembership.GROUP_ID, group.id)
|
||||
}
|
||||
)
|
||||
|
||||
group.clearDirty(null, null)
|
||||
|
||||
// cached group membership should be gone
|
||||
ab.provider!!.query(
|
||||
ab.syncAdapterURI(ContactsContract.Data.CONTENT_URI), arrayOf(CachedGroupMembership.GROUP_ID, CachedGroupMembership.RAW_CONTACT_ID),
|
||||
"${CachedGroupMembership.MIMETYPE}=?", arrayOf(CachedGroupMembership.CONTENT_ITEM_TYPE),
|
||||
null
|
||||
)!!.use { cursor ->
|
||||
assertFalse(cursor.moveToNext())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testMarkMembersDirty() {
|
||||
val ab = addressBookGroupsAsCategories
|
||||
val group = newGroup(ab)
|
||||
|
||||
val contact1 = LocalContact(ab, Contact().apply { displayName = "Test" }, "fn.vcf", null, 0)
|
||||
contact1.add()
|
||||
|
||||
val batch = BatchOperation(ab.provider!!)
|
||||
contact1.addToGroup(batch, group.id!!)
|
||||
batch.commit()
|
||||
|
||||
assertEquals(0, ab.findDirty().size)
|
||||
group.markMembersDirty()
|
||||
assertEquals(contact1.id, ab.findDirty().first().id)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testPrepareForUpload() {
|
||||
val group = newGroup()
|
||||
assertNull(group.getContact().uid)
|
||||
|
||||
val fileName = group.prepareForUpload()
|
||||
val newUid = group.getContact().uid
|
||||
assertNotNull(newUid)
|
||||
assertEquals("$newUid.vcf", fileName)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.resource
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.Context
|
||||
import at.bitfire.vcard4android.GroupMethod
|
||||
|
||||
class LocalTestAddressBook(
|
||||
context: Context,
|
||||
provider: ContentProviderClient,
|
||||
override val groupMethod: GroupMethod
|
||||
): LocalAddressBook(context, ACCOUNT, provider) {
|
||||
|
||||
companion object {
|
||||
val ACCOUNT = Account("LocalTestAddressBook", "at.bitfire.davdroid.test")
|
||||
}
|
||||
|
||||
override var mainAccount: Account?
|
||||
get() = throw NotImplementedError()
|
||||
set(_) = throw NotImplementedError()
|
||||
|
||||
override var readOnly: Boolean
|
||||
get() = false
|
||||
set(_) = throw NotImplementedError()
|
||||
|
||||
|
||||
fun clear() {
|
||||
for (contact in queryContacts(null, null))
|
||||
contact.delete()
|
||||
for (group in queryGroups(null, null))
|
||||
group.delete()
|
||||
}
|
||||
|
||||
}
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.resource.contactrow
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.ContentValues
|
||||
import android.provider.ContactsContract
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
import at.bitfire.davdroid.resource.LocalContact
|
||||
import at.bitfire.davdroid.resource.LocalTestAddressBook
|
||||
import at.bitfire.vcard4android.CachedGroupMembership
|
||||
import at.bitfire.vcard4android.Contact
|
||||
import at.bitfire.vcard4android.GroupMethod
|
||||
import org.junit.*
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
|
||||
class CachedGroupMembershipHandlerTest {
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmField
|
||||
@ClassRule
|
||||
val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!!
|
||||
|
||||
private lateinit var provider: ContentProviderClient
|
||||
private lateinit var addressBook: LocalTestAddressBook
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun connect() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!!
|
||||
Assert.assertNotNull(provider)
|
||||
|
||||
addressBook = LocalTestAddressBook(context, provider, GroupMethod.GROUP_VCARDS)
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@JvmStatic
|
||||
fun disconnect() {
|
||||
@Suppress("DEPRECATION")
|
||||
provider.release()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testMembership() {
|
||||
val contact = Contact()
|
||||
val localContact = LocalContact(addressBook, contact, null, null, 0)
|
||||
CachedGroupMembershipHandler(localContact).handle(ContentValues().apply {
|
||||
put(CachedGroupMembership.GROUP_ID, 123456)
|
||||
put(CachedGroupMembership.RAW_CONTACT_ID, 789)
|
||||
}, contact)
|
||||
assertArrayEquals(arrayOf(123456L), localContact.cachedGroupMemberships.toArray())
|
||||
}
|
||||
|
||||
}
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.resource.contactrow
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ContentProviderClient
|
||||
import android.net.Uri
|
||||
import android.provider.ContactsContract
|
||||
import android.provider.ContactsContract.CommonDataKinds.GroupMembership
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
import at.bitfire.davdroid.resource.LocalTestAddressBook
|
||||
import at.bitfire.vcard4android.Contact
|
||||
import at.bitfire.vcard4android.GroupMethod
|
||||
import org.junit.*
|
||||
import org.junit.Assert.assertEquals
|
||||
|
||||
class GroupMembershipBuilderTest {
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
@ClassRule
|
||||
val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!!
|
||||
|
||||
private lateinit var provider: ContentProviderClient
|
||||
|
||||
private lateinit var addressBookGroupsAsCategories: LocalTestAddressBook
|
||||
private lateinit var addressBookGroupsAsVCards: LocalTestAddressBook
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun connect() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!!
|
||||
Assert.assertNotNull(provider)
|
||||
|
||||
addressBookGroupsAsCategories = LocalTestAddressBook(context, provider, GroupMethod.CATEGORIES)
|
||||
addressBookGroupsAsVCards = LocalTestAddressBook(context, provider, GroupMethod.GROUP_VCARDS)
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@JvmStatic
|
||||
fun disconnect() {
|
||||
@Suppress("DEPRECATION")
|
||||
provider.release()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testCategories_GroupsAsCategories() {
|
||||
val contact = Contact().apply {
|
||||
categories += "TEST GROUP"
|
||||
}
|
||||
GroupMembershipBuilder(Uri.EMPTY, null, contact, addressBookGroupsAsCategories, false).build().also { result ->
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(GroupMembership.CONTENT_ITEM_TYPE, result[0].values[GroupMembership.MIMETYPE])
|
||||
assertEquals(addressBookGroupsAsCategories.findOrCreateGroup("TEST GROUP"), result[0].values[GroupMembership.GROUP_ROW_ID])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCategories_GroupsAsVCards() {
|
||||
val contact = Contact().apply {
|
||||
categories += "TEST GROUP"
|
||||
}
|
||||
GroupMembershipBuilder(Uri.EMPTY, null, contact, addressBookGroupsAsVCards, false).build().also { result ->
|
||||
// group membership is constructed during post-processing
|
||||
assertEquals(0, result.size)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.resource.contactrow
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.ContentValues
|
||||
import android.provider.ContactsContract
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
import at.bitfire.davdroid.resource.LocalContact
|
||||
import at.bitfire.davdroid.resource.LocalTestAddressBook
|
||||
import at.bitfire.vcard4android.CachedGroupMembership
|
||||
import at.bitfire.vcard4android.Contact
|
||||
import at.bitfire.vcard4android.GroupMethod
|
||||
import org.junit.*
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
|
||||
class GroupMembershipHandlerTest {
|
||||
|
||||
@JvmField
|
||||
@Rule
|
||||
val permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)!!
|
||||
|
||||
private lateinit var provider: ContentProviderClient
|
||||
|
||||
private lateinit var addressBookGroupsAsCategories: LocalTestAddressBook
|
||||
private var addressBookGroupsAsCategoriesGroup: Long = -1
|
||||
|
||||
private lateinit var addressBookGroupsAsVCards: LocalTestAddressBook
|
||||
|
||||
@Before
|
||||
fun connect() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!!
|
||||
Assert.assertNotNull(provider)
|
||||
|
||||
addressBookGroupsAsCategories = LocalTestAddressBook(context, provider, GroupMethod.CATEGORIES)
|
||||
addressBookGroupsAsCategoriesGroup = addressBookGroupsAsCategories.findOrCreateGroup("TEST GROUP")
|
||||
|
||||
addressBookGroupsAsVCards = LocalTestAddressBook(context, provider, GroupMethod.GROUP_VCARDS)
|
||||
}
|
||||
|
||||
@After
|
||||
fun disconnect() {
|
||||
@Suppress("DEPRECATION")
|
||||
provider.release()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testMembership_GroupsAsCategories() {
|
||||
val contact = Contact()
|
||||
val localContact = LocalContact(addressBookGroupsAsCategories, contact, null, null, 0)
|
||||
GroupMembershipHandler(localContact).handle(ContentValues().apply {
|
||||
put(CachedGroupMembership.GROUP_ID, addressBookGroupsAsCategoriesGroup)
|
||||
put(CachedGroupMembership.RAW_CONTACT_ID, -1)
|
||||
}, contact)
|
||||
assertArrayEquals(arrayOf(addressBookGroupsAsCategoriesGroup), localContact.groupMemberships.toArray())
|
||||
assertArrayEquals(arrayOf("TEST GROUP"), contact.categories.toArray())
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testMembership_GroupsAsVCards() {
|
||||
val contact = Contact()
|
||||
val localContact = LocalContact(addressBookGroupsAsVCards, contact, null, null, 0)
|
||||
GroupMembershipHandler(localContact).handle(ContentValues().apply {
|
||||
put(CachedGroupMembership.GROUP_ID, 12345) // because the group name is not queried and put into CATEGORIES, the group doesn't have to really exist
|
||||
put(CachedGroupMembership.RAW_CONTACT_ID, -1)
|
||||
}, contact)
|
||||
assertArrayEquals(arrayOf(12345L), localContact.groupMemberships.toArray())
|
||||
assertTrue(contact.categories.isEmpty())
|
||||
}
|
||||
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.resource.contactrow
|
||||
|
||||
import android.net.Uri
|
||||
import at.bitfire.vcard4android.Contact
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class UnknownPropertiesBuilderTest {
|
||||
|
||||
@Test
|
||||
fun testUnknownProperties_None() {
|
||||
UnknownPropertiesBuilder(Uri.EMPTY, null, Contact(), false).build().also { result ->
|
||||
assertEquals(0, result.size)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUnknownProperties_Properties() {
|
||||
UnknownPropertiesBuilder(Uri.EMPTY, null, Contact().apply {
|
||||
unknownProperties = "X-TEST:12345"
|
||||
}, false).build().also { result ->
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(UnknownProperties.CONTENT_ITEM_TYPE, result[0].values[UnknownProperties.MIMETYPE])
|
||||
assertEquals("X-TEST:12345", result[0].values[UnknownProperties.UNKNOWN_PROPERTIES])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.resource.contactrow
|
||||
|
||||
import android.content.ContentValues
|
||||
import at.bitfire.vcard4android.Contact
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class UnknownPropertiesHandlerTest {
|
||||
|
||||
@Test
|
||||
fun testUnknownProperties_Empty() {
|
||||
val contact = Contact()
|
||||
UnknownPropertiesHandler.handle(ContentValues().apply {
|
||||
putNull(UnknownProperties.UNKNOWN_PROPERTIES)
|
||||
}, contact)
|
||||
assertNull(contact.unknownProperties)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUnknownProperties_Values() {
|
||||
val contact = Contact()
|
||||
UnknownPropertiesHandler.handle(ContentValues().apply {
|
||||
put(UnknownProperties.UNKNOWN_PROPERTIES, "X-TEST:12345")
|
||||
}, contact)
|
||||
assertEquals("X-TEST:12345", contact.unknownProperties)
|
||||
}
|
||||
|
||||
}
|
||||
-227
@@ -1,227 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.servicedetection
|
||||
|
||||
import android.security.NetworkSecurityPolicy
|
||||
import androidx.test.filters.SmallTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.dav4jvm.DavResource
|
||||
import at.bitfire.dav4jvm.property.carddav.AddressbookHomeSet
|
||||
import at.bitfire.dav4jvm.property.webdav.ResourceType
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.servicedetection.DavResourceFinder.Configuration.ServiceInfo
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import okhttp3.mockwebserver.Dispatcher
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okhttp3.mockwebserver.RecordedRequest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assume
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.net.URI
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
class DavResourceFinderTest {
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Inject
|
||||
lateinit var settingsManager: SettingsManager
|
||||
|
||||
@Before
|
||||
fun inject() {
|
||||
hiltRule.inject()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PATH_NO_DAV = "/nodav"
|
||||
private const val PATH_CALDAV = "/caldav"
|
||||
private const val PATH_CARDDAV = "/carddav"
|
||||
private const val PATH_CALDAV_AND_CARDDAV = "/both-caldav-carddav"
|
||||
|
||||
private const val SUBPATH_PRINCIPAL = "/principal"
|
||||
private const val SUBPATH_ADDRESSBOOK_HOMESET = "/addressbooks"
|
||||
private const val SUBPATH_ADDRESSBOOK = "/addressbooks/private-contacts"
|
||||
}
|
||||
|
||||
val server = MockWebServer()
|
||||
|
||||
lateinit var finder: DavResourceFinder
|
||||
lateinit var client: HttpClient
|
||||
|
||||
@Before
|
||||
fun initServerAndClient() {
|
||||
server.dispatcher = TestDispatcher()
|
||||
server.start()
|
||||
|
||||
val baseURI = URI.create("/")
|
||||
val credentials = Credentials("mock", "12345")
|
||||
|
||||
finder = DavResourceFinder(InstrumentationRegistry.getInstrumentation().targetContext, baseURI, credentials)
|
||||
client = HttpClient.Builder(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||
.addAuthentication(null, credentials)
|
||||
.build()
|
||||
|
||||
Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
|
||||
}
|
||||
|
||||
@After
|
||||
fun stopServer() {
|
||||
server.shutdown()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun testRememberIfAddressBookOrHomeset() {
|
||||
// recognize home set
|
||||
var info = ServiceInfo()
|
||||
DavResource(client.okHttpClient, server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL))
|
||||
.propfind(0, AddressbookHomeSet.NAME) { response, _ ->
|
||||
finder.scanResponse(ResourceType.ADDRESSBOOK, response, info)
|
||||
}
|
||||
assertEquals(0, info.collections.size)
|
||||
assertEquals(1, info.homeSets.size)
|
||||
assertEquals(server.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET/"), info.homeSets.first())
|
||||
|
||||
// recognize address book
|
||||
info = ServiceInfo()
|
||||
DavResource(client.okHttpClient, server.url(PATH_CARDDAV + SUBPATH_ADDRESSBOOK))
|
||||
.propfind(0, ResourceType.NAME) { response, _ ->
|
||||
finder.scanResponse(ResourceType.ADDRESSBOOK, response, info)
|
||||
}
|
||||
assertEquals(1, info.collections.size)
|
||||
assertEquals(server.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), info.collections.keys.first())
|
||||
assertEquals(0, info.homeSets.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testProvidesService() {
|
||||
assertFalse(finder.providesService(server.url(PATH_NO_DAV), DavResourceFinder.Service.CALDAV))
|
||||
assertFalse(finder.providesService(server.url(PATH_NO_DAV), DavResourceFinder.Service.CARDDAV))
|
||||
|
||||
assertTrue(finder.providesService(server.url(PATH_CALDAV), DavResourceFinder.Service.CALDAV))
|
||||
assertFalse(finder.providesService(server.url(PATH_CALDAV), DavResourceFinder.Service.CARDDAV))
|
||||
|
||||
assertTrue(finder.providesService(server.url(PATH_CARDDAV), DavResourceFinder.Service.CARDDAV))
|
||||
assertFalse(finder.providesService(server.url(PATH_CARDDAV), DavResourceFinder.Service.CALDAV))
|
||||
|
||||
assertTrue(finder.providesService(server.url(PATH_CALDAV_AND_CARDDAV), DavResourceFinder.Service.CALDAV))
|
||||
assertTrue(finder.providesService(server.url(PATH_CALDAV_AND_CARDDAV), DavResourceFinder.Service.CARDDAV))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetCurrentUserPrincipal() {
|
||||
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_NO_DAV), DavResourceFinder.Service.CALDAV))
|
||||
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_NO_DAV), DavResourceFinder.Service.CARDDAV))
|
||||
|
||||
assertEquals(
|
||||
server.url(PATH_CALDAV + SUBPATH_PRINCIPAL),
|
||||
finder.getCurrentUserPrincipal(server.url(PATH_CALDAV), DavResourceFinder.Service.CALDAV)
|
||||
)
|
||||
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_CALDAV), DavResourceFinder.Service.CARDDAV))
|
||||
|
||||
assertEquals(
|
||||
server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL),
|
||||
finder.getCurrentUserPrincipal(server.url(PATH_CARDDAV), DavResourceFinder.Service.CARDDAV)
|
||||
)
|
||||
assertNull(finder.getCurrentUserPrincipal(server.url(PATH_CARDDAV), DavResourceFinder.Service.CALDAV))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testQueryEmailAddress() {
|
||||
var info = ServiceInfo()
|
||||
assertArrayEquals(
|
||||
arrayOf("email1@example.com", "email2@example.com"),
|
||||
finder.queryEmailAddress(server.url(PATH_CALDAV + SUBPATH_PRINCIPAL)).toTypedArray()
|
||||
)
|
||||
assertTrue(finder.queryEmailAddress(server.url(PATH_CARDDAV + SUBPATH_PRINCIPAL)).isEmpty())
|
||||
}
|
||||
|
||||
|
||||
// mock server
|
||||
|
||||
class TestDispatcher: Dispatcher() {
|
||||
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
if (!checkAuth(request)) {
|
||||
val authenticate = MockResponse().setResponseCode(401)
|
||||
authenticate.setHeader("WWW-Authenticate", "Basic realm=\"test\"")
|
||||
return authenticate
|
||||
}
|
||||
|
||||
val path = request.path!!
|
||||
|
||||
if (request.method.equals("OPTIONS", true)) {
|
||||
val dav = when {
|
||||
path.startsWith(PATH_CALDAV) -> "calendar-access"
|
||||
path.startsWith(PATH_CARDDAV) -> "addressbook"
|
||||
path.startsWith(PATH_CALDAV_AND_CARDDAV) -> "calendar-access, addressbook"
|
||||
else -> null
|
||||
}
|
||||
val response = MockResponse().setResponseCode(200)
|
||||
if (dav != null)
|
||||
response.addHeader("DAV", dav)
|
||||
return response
|
||||
} else if (request.method.equals("PROPFIND", true)) {
|
||||
val props: String?
|
||||
when (path) {
|
||||
PATH_CALDAV,
|
||||
PATH_CARDDAV ->
|
||||
props = "<current-user-principal><href>$path$SUBPATH_PRINCIPAL</href></current-user-principal>"
|
||||
|
||||
PATH_CARDDAV + SUBPATH_PRINCIPAL ->
|
||||
props = "<CARD:addressbook-home-set>" +
|
||||
" <href>$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET</href>" +
|
||||
"</CARD:addressbook-home-set>"
|
||||
|
||||
PATH_CARDDAV + SUBPATH_ADDRESSBOOK ->
|
||||
props = "<resourcetype>" +
|
||||
" <collection/>" +
|
||||
" <CARD:addressbook/>" +
|
||||
"</resourcetype>"
|
||||
|
||||
PATH_CALDAV + SUBPATH_PRINCIPAL ->
|
||||
props = "<CAL:calendar-user-address-set>" +
|
||||
" <href>urn:unknown-entry</href>" +
|
||||
" <href>mailto:email1@example.com</href>" +
|
||||
" <href>mailto:email2@example.com</href>" +
|
||||
"</CAL:calendar-user-address-set>"
|
||||
|
||||
else -> props = null
|
||||
}
|
||||
Logger.log.info("Sending props: $props")
|
||||
return MockResponse()
|
||||
.setResponseCode(207)
|
||||
.setBody("<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav' xmlns:CAL='urn:ietf:params:xml:ns:caldav'>" +
|
||||
"<response>" +
|
||||
" <href>${request.path}</href>" +
|
||||
" <propstat><prop>$props</prop></propstat>" +
|
||||
"</response>" +
|
||||
"</multistatus>")
|
||||
}
|
||||
|
||||
return MockResponse().setResponseCode(404)
|
||||
}
|
||||
|
||||
private fun checkAuth(rq: RecordedRequest) =
|
||||
rq.getHeader("Authorization") == "Basic bW9jazoxMjM0NQ=="
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
-755
@@ -1,755 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.servicedetection
|
||||
|
||||
import android.content.Context
|
||||
import android.security.NetworkSecurityPolicy
|
||||
import android.util.Log
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.testing.WorkManagerTestInitHelper
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.HomeSet
|
||||
import at.bitfire.davdroid.db.Principal
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.settings.Settings
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.mockk.every
|
||||
import io.mockk.junit4.MockKRule
|
||||
import io.mockk.mockk
|
||||
import okhttp3.mockwebserver.Dispatcher
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okhttp3.mockwebserver.RecordedRequest
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assume
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
class RefreshCollectionsWorkerTest {
|
||||
|
||||
@get:Rule
|
||||
var hiltRule = HiltAndroidRule(this)
|
||||
@get:Rule
|
||||
val mockkRule = MockKRule(this)
|
||||
|
||||
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
@Inject
|
||||
lateinit var workerFactory: HiltWorkerFactory
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
hiltRule.inject()
|
||||
|
||||
// The test application is an instance of HiltTestApplication, which doesn't initialize notification channels.
|
||||
// However, we need notification channels for the ongoing work notifications.
|
||||
NotificationUtils.createChannels(context)
|
||||
|
||||
// Initialize WorkManager for instrumentation tests.
|
||||
val config = Configuration.Builder()
|
||||
.setMinimumLoggingLevel(Log.DEBUG)
|
||||
.setWorkerFactory(workerFactory)
|
||||
.build()
|
||||
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
|
||||
}
|
||||
|
||||
|
||||
// Test dependencies
|
||||
|
||||
companion object {
|
||||
private const val PATH_CALDAV = "/caldav"
|
||||
private const val PATH_CARDDAV = "/carddav"
|
||||
|
||||
private const val SUBPATH_PRINCIPAL = "/principal"
|
||||
private const val SUBPATH_PRINCIPAL_INACCESSIBLE = "/inaccessible-principal"
|
||||
private const val SUBPATH_PRINCIPAL_WITHOUT_COLLECTIONS = "/principal2"
|
||||
private const val SUBPATH_ADDRESSBOOK_HOMESET = "/addressbooks-homeset"
|
||||
private const val SUBPATH_ADDRESSBOOK_HOMESET_EMPTY = "/addressbooks-homeset-empty"
|
||||
private const val SUBPATH_ADDRESSBOOK = "/addressbooks/my-contacts"
|
||||
private const val SUBPATH_ADDRESSBOOK_INACCESSIBLE = "/addressbooks/inaccessible-contacts"
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
|
||||
@Inject
|
||||
lateinit var settings: SettingsManager
|
||||
|
||||
var mockServer = MockWebServer()
|
||||
|
||||
lateinit var client: HttpClient
|
||||
|
||||
@Before
|
||||
fun mockServerSetup() {
|
||||
// Start mock web server
|
||||
mockServer.dispatcher = TestDispatcher()
|
||||
mockServer.start()
|
||||
|
||||
client = HttpClient.Builder(InstrumentationRegistry.getInstrumentation().targetContext).build()
|
||||
|
||||
Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
mockServer.shutdown()
|
||||
db.close()
|
||||
}
|
||||
|
||||
|
||||
// Actual tests
|
||||
|
||||
/* Often fails for unknown reasons:
|
||||
@Test
|
||||
fun testRefreshCollections_enqueuesWorker() {
|
||||
val service = createTestService(Service.TYPE_CALDAV)!!
|
||||
|
||||
val (workerName, enqueueOp) = RefreshCollectionsWorker.enqueue(context, service.id)
|
||||
enqueueOp.result.get()
|
||||
assertTrue(workScheduledOrRunningOrSuccessful(context, workerName))
|
||||
}*/
|
||||
|
||||
@Test
|
||||
fun testDiscoverHomesets() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
val baseUrl = mockServer.url(PATH_CARDDAV + SUBPATH_PRINCIPAL)
|
||||
|
||||
// Query home sets
|
||||
RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
.discoverHomesets(baseUrl)
|
||||
|
||||
// Check home sets have been saved to database
|
||||
assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET/"), db.homeSetDao().getByService(service.id).first().url)
|
||||
assertEquals(1, db.homeSetDao().getByService(service.id).size)
|
||||
}
|
||||
|
||||
|
||||
// refreshHomesetsAndTheirCollections
|
||||
|
||||
@Test
|
||||
fun refreshHomesetsAndTheirCollections_addsNewCollection() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
|
||||
// save homeset in DB
|
||||
val homesetId = db.homeSetDao().insert(
|
||||
HomeSet(id=0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET"))
|
||||
)
|
||||
|
||||
// Refresh
|
||||
RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
.refreshHomesetsAndTheirCollections()
|
||||
|
||||
// Check the collection defined in homeset is now in the database
|
||||
assertEquals(
|
||||
Collection(
|
||||
1,
|
||||
service.id,
|
||||
homesetId,
|
||||
1, // will have gotten an owner too
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
|
||||
displayName = "My Contacts",
|
||||
description = "My Contacts Description"
|
||||
),
|
||||
db.collectionDao().getByService(service.id).first()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun refreshHomesetsAndTheirCollections_updatesExistingCollection() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
|
||||
// save "old" collection in DB
|
||||
val collectionId = db.collectionDao().insertOrUpdateByUrl(
|
||||
Collection(
|
||||
0,
|
||||
service.id,
|
||||
null,
|
||||
null,
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
|
||||
displayName = "My Contacts",
|
||||
description = "My Contacts Description"
|
||||
)
|
||||
)
|
||||
|
||||
// Refresh
|
||||
RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
.refreshHomesetsAndTheirCollections()
|
||||
|
||||
// Check the collection got updated
|
||||
assertEquals(
|
||||
Collection(
|
||||
collectionId,
|
||||
service.id,
|
||||
null,
|
||||
null,
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
|
||||
displayName = "My Contacts",
|
||||
description = "My Contacts Description"
|
||||
),
|
||||
db.collectionDao().get(collectionId)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun refreshHomesetsAndTheirCollections_preservesCollectionFlags() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
|
||||
// save "old" collection in DB - with set flags
|
||||
val collectionId = db.collectionDao().insertOrUpdateByUrl(
|
||||
Collection(
|
||||
0,
|
||||
service.id,
|
||||
null,
|
||||
null,
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
|
||||
displayName = "My Contacts",
|
||||
description = "My Contacts Description",
|
||||
forceReadOnly = true,
|
||||
sync = true
|
||||
)
|
||||
)
|
||||
|
||||
// Refresh
|
||||
RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
.refreshHomesetsAndTheirCollections()
|
||||
|
||||
// Check the collection got updated
|
||||
assertEquals(
|
||||
Collection(
|
||||
collectionId,
|
||||
service.id,
|
||||
null,
|
||||
null,
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
|
||||
displayName = "My Contacts",
|
||||
description = "My Contacts Description",
|
||||
forceReadOnly = true,
|
||||
sync = true
|
||||
),
|
||||
db.collectionDao().get(collectionId)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun refreshHomesetsAndTheirCollections_marksRemovedCollectionsAsHomeless() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
|
||||
// save homeset in DB - which is empty (zero address books) on the serverside
|
||||
val homesetId = db.homeSetDao().insert(
|
||||
HomeSet(id=0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_EMPTY"))
|
||||
)
|
||||
|
||||
// place collection in DB - as part of the homeset
|
||||
val collectionId = db.collectionDao().insertOrUpdateByUrl(
|
||||
Collection(
|
||||
0,
|
||||
service.id,
|
||||
homesetId,
|
||||
null,
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/")
|
||||
)
|
||||
)
|
||||
|
||||
// Refresh - should mark collection as homeless, because serverside homeset is empty
|
||||
RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
.refreshHomesetsAndTheirCollections()
|
||||
|
||||
// Check the collection, is now marked as homeless
|
||||
assertEquals(null, db.collectionDao().get(collectionId)!!.homeSetId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun refreshHomesetsAndTheirCollections_addsOwnerUrls() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
|
||||
// save a homeset in DB
|
||||
val homesetId = db.homeSetDao().insert(
|
||||
HomeSet(id=0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET"))
|
||||
)
|
||||
|
||||
// place collection in DB - as part of the homeset
|
||||
val collectionId = db.collectionDao().insertOrUpdateByUrl(
|
||||
Collection(
|
||||
0,
|
||||
service.id,
|
||||
homesetId, // part of above home set
|
||||
null,
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/")
|
||||
)
|
||||
)
|
||||
|
||||
// Refresh - homesets and their collections
|
||||
assertEquals(0, db.principalDao().getByService(service.id).size)
|
||||
RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
.refreshHomesetsAndTheirCollections()
|
||||
|
||||
// Check principal saved and the collection was updated with its reference
|
||||
val principals = db.principalDao().getByService(service.id)
|
||||
assertEquals(1, principals.size)
|
||||
assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL"), principals[0].url)
|
||||
assertEquals(null, principals[0].displayName)
|
||||
assertEquals(
|
||||
principals[0].id,
|
||||
db.collectionDao().get(collectionId)!!.ownerId
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// refreshHomelessCollections
|
||||
|
||||
@Test
|
||||
fun refreshHomelessCollections_updatesExistingCollection() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
|
||||
// place homeless collection in DB
|
||||
val collectionId = db.collectionDao().insertOrUpdateByUrl(
|
||||
Collection(
|
||||
0,
|
||||
service.id,
|
||||
null,
|
||||
null,
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
|
||||
)
|
||||
)
|
||||
|
||||
// Refresh
|
||||
RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
.refreshHomelessCollections()
|
||||
|
||||
// Check the collection got updated - with display name and description
|
||||
assertEquals(
|
||||
Collection(
|
||||
collectionId,
|
||||
service.id,
|
||||
null,
|
||||
1, // will have gotten an owner too
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
|
||||
displayName = "My Contacts",
|
||||
description = "My Contacts Description"
|
||||
),
|
||||
db.collectionDao().get(collectionId)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun refreshHomelessCollections_deletesInaccessibleCollections() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
|
||||
// place homeless collection in DB - it is also inaccessible
|
||||
val collectionId = db.collectionDao().insertOrUpdateByUrl(
|
||||
Collection(
|
||||
0,
|
||||
service.id,
|
||||
null,
|
||||
null,
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_INACCESSIBLE")
|
||||
)
|
||||
)
|
||||
|
||||
// Refresh - should delete collection
|
||||
RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
.refreshHomelessCollections()
|
||||
|
||||
// Check the collection got deleted
|
||||
assertEquals(null, db.collectionDao().get(collectionId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun refreshHomelessCollections_addsOwnerUrls() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
|
||||
// place homeless collection in DB
|
||||
val collectionId = db.collectionDao().insertOrUpdateByUrl(
|
||||
Collection(
|
||||
0,
|
||||
service.id,
|
||||
null,
|
||||
null,
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"),
|
||||
)
|
||||
)
|
||||
|
||||
// Refresh homeless collections
|
||||
assertEquals(0, db.principalDao().getByService(service.id).size)
|
||||
RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
.refreshHomelessCollections()
|
||||
|
||||
// Check principal saved and the collection was updated with its reference
|
||||
val principals = db.principalDao().getByService(service.id)
|
||||
assertEquals(1, principals.size)
|
||||
assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL"), principals[0].url)
|
||||
assertEquals(null, principals[0].displayName)
|
||||
assertEquals(
|
||||
principals[0].id,
|
||||
db.collectionDao().get(collectionId)!!.ownerId
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// refreshPrincipals
|
||||
|
||||
@Test
|
||||
fun refreshPrincipals_inaccessiblePrincipal() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
|
||||
// place principal without display name in db
|
||||
val principalId = db.principalDao().insert(
|
||||
Principal(
|
||||
0,
|
||||
service.id,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL_INACCESSIBLE"), // no trailing slash
|
||||
null // no display name for now
|
||||
)
|
||||
)
|
||||
// add an associated collection - as the principal is rightfully removed otherwise
|
||||
db.collectionDao().insertOrUpdateByUrl(
|
||||
Collection(
|
||||
0,
|
||||
service.id,
|
||||
null,
|
||||
principalId, // create association with principal
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), // with trailing slash
|
||||
)
|
||||
)
|
||||
|
||||
// Refresh principals
|
||||
RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
.refreshPrincipals()
|
||||
|
||||
// Check principal was not updated
|
||||
val principals = db.principalDao().getByService(service.id)
|
||||
assertEquals(1, principals.size)
|
||||
assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL_INACCESSIBLE"), principals[0].url)
|
||||
assertEquals(null, principals[0].displayName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun refreshPrincipals_updatesPrincipal() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
|
||||
// place principal without display name in db
|
||||
val principalId = db.principalDao().insert(
|
||||
Principal(
|
||||
0,
|
||||
service.id,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL"), // no trailing slash
|
||||
null // no display name for now
|
||||
)
|
||||
)
|
||||
// add an associated collection - as the principal is rightfully removed otherwise
|
||||
db.collectionDao().insertOrUpdateByUrl(
|
||||
Collection(
|
||||
0,
|
||||
service.id,
|
||||
null,
|
||||
principalId, // create association with principal
|
||||
Collection.TYPE_ADDRESSBOOK,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/"), // with trailing slash
|
||||
)
|
||||
)
|
||||
|
||||
// Refresh principals
|
||||
RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
.refreshPrincipals()
|
||||
|
||||
// Check principal now got a display name
|
||||
val principals = db.principalDao().getByService(service.id)
|
||||
assertEquals(1, principals.size)
|
||||
assertEquals(mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL"), principals[0].url)
|
||||
assertEquals("Mr. Wobbles", principals[0].displayName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun refreshPrincipals_deletesPrincipalsWithoutCollections() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
|
||||
// place principal without collections in DB
|
||||
db.principalDao().insert(
|
||||
Principal(
|
||||
0,
|
||||
service.id,
|
||||
mockServer.url("$PATH_CARDDAV$SUBPATH_PRINCIPAL_WITHOUT_COLLECTIONS/")
|
||||
)
|
||||
)
|
||||
|
||||
// Refresh principals - detecting it does not own collections
|
||||
RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
.refreshPrincipals()
|
||||
|
||||
// Check principal was deleted
|
||||
val principals = db.principalDao().getByService(service.id)
|
||||
assertEquals(0, principals.size)
|
||||
}
|
||||
|
||||
// Others
|
||||
|
||||
@Test
|
||||
fun shouldPreselect_none() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
|
||||
val settings = mockk<SettingsManager>()
|
||||
every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_NONE
|
||||
every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns ""
|
||||
|
||||
val collection = Collection(
|
||||
0,
|
||||
service.id,
|
||||
0,
|
||||
type = Collection.TYPE_ADDRESSBOOK,
|
||||
url = mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/")
|
||||
)
|
||||
val homesets = listOf(
|
||||
HomeSet(0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET"))
|
||||
)
|
||||
|
||||
val refresher = RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
assertFalse(refresher.shouldPreselect(collection, homesets))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldPreselect_all() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
|
||||
val settings = mockk<SettingsManager>()
|
||||
every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_ALL
|
||||
every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns ""
|
||||
|
||||
val collection = Collection(
|
||||
0,
|
||||
service.id,
|
||||
0,
|
||||
type = Collection.TYPE_ADDRESSBOOK,
|
||||
url = mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/")
|
||||
)
|
||||
val homesets = listOf(
|
||||
HomeSet(0, service.id, false, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET"))
|
||||
)
|
||||
|
||||
val refresher = RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
assertTrue(refresher.shouldPreselect(collection, homesets))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldPreselect_all_blacklisted() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
val url = mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/")
|
||||
|
||||
val settings = mockk<SettingsManager>()
|
||||
every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_ALL
|
||||
every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns url.toString()
|
||||
|
||||
val collection = Collection(
|
||||
0,
|
||||
service.id,
|
||||
0,
|
||||
type = Collection.TYPE_ADDRESSBOOK,
|
||||
url = url
|
||||
)
|
||||
val homesets = listOf(
|
||||
HomeSet(0, service.id, false, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET"))
|
||||
)
|
||||
|
||||
val refresher = RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
assertFalse(refresher.shouldPreselect(collection, homesets))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldPreselect_personal_notPersonal() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
|
||||
val settings = mockk<SettingsManager>()
|
||||
every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_PERSONAL
|
||||
every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns ""
|
||||
|
||||
val collection = Collection(
|
||||
0,
|
||||
service.id,
|
||||
0,
|
||||
type = Collection.TYPE_ADDRESSBOOK,
|
||||
url = mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/")
|
||||
)
|
||||
val homesets = listOf(
|
||||
HomeSet(0, service.id, false, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET"))
|
||||
)
|
||||
|
||||
val refresher = RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
assertFalse(refresher.shouldPreselect(collection, homesets))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldPreselect_personal_isPersonal() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
|
||||
val settings = mockk<SettingsManager>()
|
||||
every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_PERSONAL
|
||||
every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns ""
|
||||
|
||||
val collection = Collection(
|
||||
0,
|
||||
service.id,
|
||||
0,
|
||||
type = Collection.TYPE_ADDRESSBOOK,
|
||||
url = mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/")
|
||||
)
|
||||
val homesets = listOf(
|
||||
HomeSet(0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET"))
|
||||
)
|
||||
|
||||
val refresher = RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
assertTrue(refresher.shouldPreselect(collection, homesets))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldPreselect_personal_isPersonalButBlacklisted() {
|
||||
val service = createTestService(Service.TYPE_CARDDAV)!!
|
||||
val collectionUrl = mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/")
|
||||
|
||||
val settings = mockk<SettingsManager>()
|
||||
every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_PERSONAL
|
||||
every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns collectionUrl.toString()
|
||||
|
||||
val collection = Collection(
|
||||
0,
|
||||
service.id,
|
||||
0,
|
||||
type = Collection.TYPE_ADDRESSBOOK,
|
||||
url = collectionUrl
|
||||
)
|
||||
val homesets = listOf(
|
||||
HomeSet(0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET"))
|
||||
)
|
||||
|
||||
val refresher = RefreshCollectionsWorker.Refresher(db, service, settings, client.okHttpClient)
|
||||
assertFalse(refresher.shouldPreselect(collection, homesets))
|
||||
}
|
||||
|
||||
// Test helpers and dependencies
|
||||
|
||||
private fun createTestService(serviceType: String) : Service? {
|
||||
val service = Service(id=0, accountName="test", type=serviceType, principal = null)
|
||||
val serviceId = db.serviceDao().insertOrReplace(service)
|
||||
return db.serviceDao().get(serviceId)
|
||||
}
|
||||
|
||||
class TestDispatcher: Dispatcher() {
|
||||
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
val path = StringUtils.removeEnd(request.path!!, "/")
|
||||
|
||||
if (request.method.equals("PROPFIND", true)) {
|
||||
val properties = when (path) {
|
||||
PATH_CALDAV,
|
||||
PATH_CARDDAV ->
|
||||
"<current-user-principal>" +
|
||||
" <href>$path${SUBPATH_PRINCIPAL}</href>" +
|
||||
"</current-user-principal>"
|
||||
|
||||
PATH_CARDDAV + SUBPATH_PRINCIPAL ->
|
||||
"<resourcetype><principal/></resourcetype>" +
|
||||
"<displayname>Mr. Wobbles</displayname>" +
|
||||
"<CARD:addressbook-home-set>" +
|
||||
" <href>${PATH_CARDDAV}${SUBPATH_ADDRESSBOOK_HOMESET}</href>" +
|
||||
"</CARD:addressbook-home-set>"
|
||||
|
||||
PATH_CARDDAV + SUBPATH_PRINCIPAL_WITHOUT_COLLECTIONS ->
|
||||
"<CARD:addressbook-home-set>" +
|
||||
" <href>${PATH_CARDDAV}${SUBPATH_ADDRESSBOOK_HOMESET_EMPTY}</href>" +
|
||||
"</CARD:addressbook-home-set>" +
|
||||
"<displayname>Mr. Wobbles Jr.</displayname>"
|
||||
|
||||
PATH_CARDDAV + SUBPATH_ADDRESSBOOK,
|
||||
PATH_CARDDAV + SUBPATH_ADDRESSBOOK_HOMESET ->
|
||||
"<resourcetype>" +
|
||||
" <collection/>" +
|
||||
" <CARD:addressbook/>" +
|
||||
"</resourcetype>" +
|
||||
"<displayname>My Contacts</displayname>" +
|
||||
"<CARD:addressbook-description>My Contacts Description</CARD:addressbook-description>" +
|
||||
"<owner>" +
|
||||
" <href>${PATH_CARDDAV + SUBPATH_PRINCIPAL}</href>" +
|
||||
"</owner>"
|
||||
|
||||
PATH_CALDAV + SUBPATH_PRINCIPAL ->
|
||||
"<CAL:calendar-user-address-set>" +
|
||||
" <href>urn:unknown-entry</href>" +
|
||||
" <href>mailto:email1@example.com</href>" +
|
||||
" <href>mailto:email2@example.com</href>" +
|
||||
"</CAL:calendar-user-address-set>"
|
||||
|
||||
SUBPATH_ADDRESSBOOK_HOMESET_EMPTY -> ""
|
||||
|
||||
else -> ""
|
||||
}
|
||||
|
||||
var responseBody = ""
|
||||
var responseCode = 207
|
||||
when (path) {
|
||||
PATH_CARDDAV + SUBPATH_ADDRESSBOOK_HOMESET ->
|
||||
responseBody =
|
||||
"<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav' xmlns:CAL='urn:ietf:params:xml:ns:caldav'>" +
|
||||
"<response>" +
|
||||
" <href>${PATH_CARDDAV + SUBPATH_ADDRESSBOOK}</href>" +
|
||||
" <propstat><prop>" +
|
||||
properties +
|
||||
" </prop></propstat>" +
|
||||
" <status>HTTP/1.1 200 OK</status>" +
|
||||
"</response>" +
|
||||
"</multistatus>"
|
||||
|
||||
PATH_CARDDAV + SUBPATH_PRINCIPAL_INACCESSIBLE,
|
||||
PATH_CARDDAV + SUBPATH_ADDRESSBOOK_INACCESSIBLE ->
|
||||
responseCode = 404
|
||||
|
||||
else ->
|
||||
responseBody =
|
||||
"<multistatus xmlns='DAV:' xmlns:CARD='urn:ietf:params:xml:ns:carddav' xmlns:CAL='urn:ietf:params:xml:ns:caldav'>" +
|
||||
"<response>" +
|
||||
" <href>$path</href>" +
|
||||
" <propstat><prop>"+
|
||||
properties +
|
||||
" </prop></propstat>" +
|
||||
"</response>" +
|
||||
"</multistatus>"
|
||||
}
|
||||
|
||||
Logger.log.info("Queried: $path")
|
||||
Logger.log.info("Response: $responseBody")
|
||||
return MockResponse()
|
||||
.setResponseCode(responseCode)
|
||||
.setBody(responseBody)
|
||||
}
|
||||
|
||||
return MockResponse().setResponseCode(404)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.settings
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.ContentResolver
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.ContactsContract
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.testing.WorkManagerTestInitHelper
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.TestUtils
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import at.bitfire.davdroid.syncadapter.AccountUtils
|
||||
import at.bitfire.davdroid.syncadapter.PeriodicSyncWorker
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import javax.inject.Inject
|
||||
|
||||
/*@HiltAndroidTest
|
||||
class AccountSettingsTest {
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Inject
|
||||
lateinit var settingsManager: SettingsManager
|
||||
|
||||
|
||||
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
val account = Account(javaClass.canonicalName, context.getString(R.string.account_type))
|
||||
val fakeCredentials = Credentials("test", "test")
|
||||
|
||||
val authorities = listOf(
|
||||
context.getString(R.string.address_books_authority),
|
||||
CalendarContract.AUTHORITY,
|
||||
TaskProvider.ProviderName.JtxBoard.authority,
|
||||
TaskProvider.ProviderName.OpenTasks.authority,
|
||||
TaskProvider.ProviderName.TasksOrg.authority
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
hiltRule.inject()
|
||||
|
||||
assertTrue(AccountUtils.createAccount(
|
||||
context,
|
||||
account,
|
||||
AccountSettings.initialUserData(fakeCredentials)
|
||||
))
|
||||
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1)
|
||||
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 0)
|
||||
|
||||
// The test application is an instance of HiltTestApplication, which doesn't initialize notification channels.
|
||||
// However, we need notification channels for the ongoing work notifications.
|
||||
NotificationUtils.createChannels(context)
|
||||
|
||||
// Initialize WorkManager for instrumentation tests.
|
||||
val config = Configuration.Builder()
|
||||
.setMinimumLoggingLevel(Log.DEBUG)
|
||||
.build()
|
||||
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
|
||||
}
|
||||
|
||||
@After
|
||||
@RequiresApi(22)
|
||||
fun removeAccount() {
|
||||
AccountManager.get(context).removeAccountExplicitly(account)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testSyncIntervals() {
|
||||
val settings = AccountSettings(context, account)
|
||||
val presetIntervals =
|
||||
context.resources.getStringArray(R.array.settings_sync_interval_seconds)
|
||||
.map { it.toLong() }
|
||||
.filter { it != AccountSettings.SYNC_INTERVAL_MANUALLY }
|
||||
for (interval in presetIntervals) {
|
||||
assertTrue(settings.setSyncInterval(CalendarContract.AUTHORITY, interval))
|
||||
assertEquals(interval, settings.getSyncInterval(CalendarContract.AUTHORITY))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSyncIntervals_Syncable() {
|
||||
val settings = AccountSettings(context, account)
|
||||
val interval = 15*60L // 15 min
|
||||
val result = settings.setSyncInterval(CalendarContract.AUTHORITY, interval)
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun testSyncIntervals_TooShort() {
|
||||
val settings = AccountSettings(context, account)
|
||||
val interval = 60L // 1 min is not supported by Android
|
||||
settings.setSyncInterval(CalendarContract.AUTHORITY, interval)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSyncIntervals_activatesPeriodicSyncWorker() {
|
||||
val settings = AccountSettings(context, account)
|
||||
val interval = 15*60L
|
||||
for (authority in authorities) {
|
||||
ContentResolver.setIsSyncable(account, authority, 1)
|
||||
assertTrue(settings.setSyncInterval(authority, interval))
|
||||
assertTrue(TestUtils.workScheduledOrRunningOrSuccessful(context, PeriodicSyncWorker.workerName(account, authority)))
|
||||
assertEquals(interval, settings.getSyncInterval(authority))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSyncIntervals_disablesPeriodicSyncWorker() {
|
||||
val settings = AccountSettings(context, account)
|
||||
val interval = AccountSettings.SYNC_INTERVAL_MANUALLY // -1
|
||||
for (authority in authorities) {
|
||||
ContentResolver.setIsSyncable(account, authority, 1)
|
||||
assertTrue(settings.setSyncInterval(authority, interval))
|
||||
assertFalse(TestUtils.workScheduledOrRunningOrSuccessful(context, PeriodicSyncWorker.workerName(account, authority)))
|
||||
assertEquals(AccountSettings.SYNC_INTERVAL_MANUALLY, settings.getSyncInterval(authority))
|
||||
}
|
||||
}
|
||||
|
||||
}*/
|
||||
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.settings
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.flow.toSet
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
class SettingsManagerTest {
|
||||
|
||||
companion object {
|
||||
/** Use this setting to test SettingsManager methods. Will be removed after every test run. */
|
||||
const val SETTING_TEST = "test"
|
||||
}
|
||||
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
@get:Rule
|
||||
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@Inject lateinit var settingsManager: SettingsManager
|
||||
|
||||
@Before
|
||||
fun inject() {
|
||||
hiltRule.inject()
|
||||
}
|
||||
|
||||
@After
|
||||
fun removeTestSetting() {
|
||||
settingsManager.remove(SETTING_TEST)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun test_containsKey_NotExisting() {
|
||||
assertFalse(settingsManager.containsKey("notExisting"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_containsKey_Existing() {
|
||||
// provided by DefaultsProvider
|
||||
assertEquals(Settings.PROXY_TYPE_SYSTEM, settingsManager.getInt(Settings.PROXY_TYPE))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun test_observerFlow_initialValue() = runBlocking {
|
||||
var counter = 0
|
||||
val live = settingsManager.observerFlow {
|
||||
if (counter++ == 0)
|
||||
23
|
||||
else
|
||||
throw AssertionError("A second value was requested")
|
||||
}
|
||||
assertEquals(23, live.first())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_observerFlow_updatedValue() = runBlocking {
|
||||
var counter = 0
|
||||
val live = settingsManager.observerFlow {
|
||||
when (counter++) {
|
||||
0 -> {
|
||||
// update some setting so that we will be called a second time
|
||||
settingsManager.putBoolean(SETTING_TEST, true)
|
||||
// and emit initial value
|
||||
23
|
||||
}
|
||||
1 -> 42 // updated value
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
}
|
||||
|
||||
val result = live.take(2).toSet()
|
||||
assertEquals(setOf(23, 42), result)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.ui
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class DebugInfoActivityTest {
|
||||
|
||||
@Test
|
||||
fun testIntentBuilder_LargeLocalResource() {
|
||||
val a = 'A'.code.toByte()
|
||||
val intent = DebugInfoActivity.IntentBuilder(InstrumentationRegistry.getInstrumentation().context)
|
||||
.withLocalResource(String(ByteArray(1024*1024) { a }))
|
||||
.build()
|
||||
val expected = StringBuilder(DebugInfoActivity.IntentBuilder.MAX_ELEMENT_SIZE)
|
||||
expected.append(String(ByteArray(DebugInfoActivity.IntentBuilder.MAX_ELEMENT_SIZE - 3) { a }))
|
||||
expected.append("...")
|
||||
assertEquals(expected.toString(), intent.getStringExtra("localResource"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIntentBuilder_LargeLogs() {
|
||||
val a = 'A'.code.toByte()
|
||||
val intent = DebugInfoActivity.IntentBuilder(InstrumentationRegistry.getInstrumentation().context)
|
||||
.withLogs(String(ByteArray(1024*1024) { a }))
|
||||
.build()
|
||||
val expected = StringBuilder(DebugInfoActivity.IntentBuilder.MAX_ELEMENT_SIZE)
|
||||
expected.append(String(ByteArray(DebugInfoActivity.IntentBuilder.MAX_ELEMENT_SIZE - 3) { a }))
|
||||
expected.append("...")
|
||||
assertEquals(expected.toString(), intent.getStringExtra("logs"))
|
||||
}
|
||||
|
||||
}
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.ui.webdav
|
||||
|
||||
import android.security.NetworkSecurityPolicy
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.db.WebDavMount
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.mockk.junit4.MockKRule
|
||||
import io.mockk.spyk
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assume
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
class AddWebdavMountActivityTest {
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
@get:Rule
|
||||
val mockkRule = MockKRule(this)
|
||||
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
hiltRule.inject()
|
||||
|
||||
model = spyk(AddWebdavMountActivity.Model(ApplicationProvider.getApplicationContext(), db))
|
||||
|
||||
Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
|
||||
}
|
||||
|
||||
|
||||
lateinit var model: AddWebdavMountActivity.Model
|
||||
val web = MockWebServer()
|
||||
|
||||
@Test
|
||||
fun testHasWebDav_NoDavHeader() {
|
||||
web.enqueue(MockResponse().setResponseCode(200))
|
||||
assertFalse(model.hasWebDav(WebDavMount(name = "Test", url = web.url("/")), null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHasWebDav_DavClass_1() {
|
||||
web.enqueue(MockResponse()
|
||||
.setResponseCode(200)
|
||||
.addHeader("DAV", "1"))
|
||||
assertTrue(model.hasWebDav(WebDavMount(name = "Test", url = web.url("/")), null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHasWebDav_DavClass_1and2() {
|
||||
web.enqueue(MockResponse()
|
||||
.setResponseCode(200)
|
||||
.addHeader("DAV", "1,2"))
|
||||
assertTrue(model.hasWebDav(WebDavMount(name = "Test", url = web.url("/")), null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHasWebDav_DavClass_2() {
|
||||
web.enqueue(MockResponse()
|
||||
.setResponseCode(200)
|
||||
.addHeader("DAV", "2"))
|
||||
assertTrue(model.hasWebDav(WebDavMount(name = "Test", url = web.url("/")), null))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.webdav
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class CredentialsStoreTest {
|
||||
|
||||
private val store = CredentialsStore(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||
|
||||
@Test
|
||||
fun testSetGetDelete() {
|
||||
store.setCredentials(0, Credentials(username = "myname", password = "12345"))
|
||||
assertEquals(Credentials(username = "myname", password = "12345"), store.getCredentials(0))
|
||||
|
||||
store.setCredentials(0, null)
|
||||
assertNull(store.getCredentials(0))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.webdav
|
||||
|
||||
import android.content.Context
|
||||
import android.security.NetworkSecurityPolicy
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.db.WebDavDocument
|
||||
import at.bitfire.davdroid.db.WebDavMount
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.mockwebserver.Dispatcher
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okhttp3.mockwebserver.RecordedRequest
|
||||
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
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
class DavDocumentsProviderTest {
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
@Inject lateinit var db: AppDatabase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
hiltRule.inject()
|
||||
}
|
||||
|
||||
private var mockServer = MockWebServer()
|
||||
|
||||
private lateinit var client: HttpClient
|
||||
|
||||
companion object {
|
||||
private const val PATH_WEBDAV_ROOT = "/webdav"
|
||||
}
|
||||
|
||||
@Before
|
||||
fun mockServerSetup() {
|
||||
// Start mock web server
|
||||
mockServer.dispatcher = TestDispatcher()
|
||||
mockServer.start()
|
||||
|
||||
client = HttpClient.Builder(InstrumentationRegistry.getInstrumentation().targetContext).build()
|
||||
|
||||
// mock server delivers HTTP without encryption
|
||||
assertTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
mockServer.shutdown()
|
||||
db.close()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testDoQueryChildren_insert() {
|
||||
// Create parent and root in database
|
||||
val id = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", mockServer.url(PATH_WEBDAV_ROOT)))
|
||||
val webDavMount = db.webDavMountDao().getById(id)
|
||||
val parent = db.webDavDocumentDao().getOrCreateRoot(webDavMount)
|
||||
val cookieStore = mutableMapOf<Long, CookieJar>()
|
||||
|
||||
// Query
|
||||
DavDocumentsProvider.DavDocumentsActor(context, db, cookieStore, CredentialsStore(context), context.getString(R.string.webdav_authority))
|
||||
.queryChildren(parent)
|
||||
|
||||
// Assert new children were inserted into db
|
||||
assertEquals(3, db.webDavDocumentDao().getChildren(parent.id).size)
|
||||
assertEquals("Secret_Document.pages", db.webDavDocumentDao().getChildren(parent.id)[0].displayName)
|
||||
assertEquals("MeowMeow_Cats.docx", db.webDavDocumentDao().getChildren(parent.id)[1].displayName)
|
||||
assertEquals("Library", db.webDavDocumentDao().getChildren(parent.id)[2].displayName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDoQueryChildren_update() {
|
||||
// Create parent and root in database
|
||||
val mountId = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", mockServer.url(PATH_WEBDAV_ROOT)))
|
||||
val webDavMount = db.webDavMountDao().getById(mountId)
|
||||
val parent = db.webDavDocumentDao().getOrCreateRoot(webDavMount)
|
||||
val cookieStore = mutableMapOf<Long, CookieJar>()
|
||||
assertEquals("Cat food storage", db.webDavDocumentDao().get(parent.id)!!.displayName)
|
||||
|
||||
// Create a folder
|
||||
val folderId = db.webDavDocumentDao().insert(
|
||||
WebDavDocument(
|
||||
0,
|
||||
mountId,
|
||||
parent.id,
|
||||
"My_Books",
|
||||
true,
|
||||
"My Books",
|
||||
)
|
||||
)
|
||||
assertEquals("My_Books", db.webDavDocumentDao().get(folderId)!!.name)
|
||||
assertEquals("My Books", db.webDavDocumentDao().get(folderId)!!.displayName)
|
||||
|
||||
// Query - should update the parent displayname and folder name
|
||||
DavDocumentsProvider.DavDocumentsActor(context, db, cookieStore, CredentialsStore(context), context.getString(R.string.webdav_authority))
|
||||
.queryChildren(parent)
|
||||
|
||||
// Assert parent and children were updated in database
|
||||
assertEquals("Cats WebDAV", db.webDavDocumentDao().get(parent.id)!!.displayName)
|
||||
assertEquals("Library", db.webDavDocumentDao().getChildren(parent.id)[2].name)
|
||||
assertEquals("Library", db.webDavDocumentDao().getChildren(parent.id)[2].displayName)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDoQueryChildren_delete() {
|
||||
// Create parent and root in database
|
||||
val mountId = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", mockServer.url(PATH_WEBDAV_ROOT)))
|
||||
val webDavMount = db.webDavMountDao().getById(mountId)
|
||||
val parent = db.webDavDocumentDao().getOrCreateRoot(webDavMount)
|
||||
val cookieStore = mutableMapOf<Long, CookieJar>()
|
||||
|
||||
// Create a folder
|
||||
val folderId = db.webDavDocumentDao().insert(
|
||||
WebDavDocument(0, mountId, parent.id, "deleteme", true, "Should be deleted")
|
||||
)
|
||||
assertEquals("deleteme", db.webDavDocumentDao().get(folderId)!!.name)
|
||||
|
||||
// Query - discovers serverside deletion
|
||||
DavDocumentsProvider.DavDocumentsActor(context, db, cookieStore, CredentialsStore(context), context.getString(R.string.webdav_authority))
|
||||
.queryChildren(parent)
|
||||
|
||||
// Assert folder got deleted
|
||||
assertEquals(null, db.webDavDocumentDao().get(folderId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDoQueryChildren_updateTwoParentsSimultaneous() {
|
||||
// Create root in database
|
||||
val mountId = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", mockServer.url(PATH_WEBDAV_ROOT)))
|
||||
val webDavMount = db.webDavMountDao().getById(mountId)
|
||||
val root = db.webDavDocumentDao().getOrCreateRoot(webDavMount)
|
||||
val cookieStore = mutableMapOf<Long, CookieJar>()
|
||||
|
||||
// Create two parents
|
||||
val parent1Id = db.webDavDocumentDao().insert(WebDavDocument(0, mountId, root.id, "parent1", true))
|
||||
val parent2Id = db.webDavDocumentDao().insert(WebDavDocument(0, mountId, root.id, "parent2", true))
|
||||
val parent1 = db.webDavDocumentDao().get(parent1Id)!!
|
||||
val parent2 = db.webDavDocumentDao().get(parent2Id)!!
|
||||
assertEquals("parent1", parent1.name)
|
||||
assertEquals("parent2", parent2.name)
|
||||
|
||||
// Query - find children of two nodes simultaneously
|
||||
DavDocumentsProvider.DavDocumentsActor(context, db, cookieStore, CredentialsStore(context), context.getString(R.string.webdav_authority))
|
||||
.queryChildren(parent1)
|
||||
DavDocumentsProvider.DavDocumentsActor(context, db, cookieStore, CredentialsStore(context), context.getString(R.string.webdav_authority))
|
||||
.queryChildren(parent2)
|
||||
|
||||
// Assert the two folders names have changed
|
||||
assertEquals("childOne.txt", db.webDavDocumentDao().getChildren(parent1Id)[0].name)
|
||||
assertEquals("childTwo.txt", db.webDavDocumentDao().getChildren(parent2Id)[0].name)
|
||||
}
|
||||
|
||||
|
||||
// mock server
|
||||
|
||||
class TestDispatcher: Dispatcher() {
|
||||
|
||||
data class Resource(
|
||||
val name: String,
|
||||
val props: String
|
||||
)
|
||||
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
val requestPath = request.path!!.trimEnd('/')
|
||||
|
||||
if (request.method.equals("PROPFIND", true)) {
|
||||
|
||||
val propsMap = mutableMapOf(
|
||||
PATH_WEBDAV_ROOT to arrayOf(
|
||||
Resource("",
|
||||
"<resourcetype><collection/></resourcetype>" +
|
||||
"<displayname>Cats WebDAV</displayname>"
|
||||
),
|
||||
Resource("Secret_Document.pages",
|
||||
"<displayname>Secret_Document.pages</displayname>",
|
||||
),
|
||||
Resource("MeowMeow_Cats.docx",
|
||||
"<displayname>MeowMeow_Cats.docx</displayname>"
|
||||
),
|
||||
Resource("Library",
|
||||
"<resourcetype><collection/></resourcetype>" +
|
||||
"<displayname>Library</displayname>"
|
||||
)
|
||||
),
|
||||
|
||||
"$PATH_WEBDAV_ROOT/parent1" to arrayOf(
|
||||
Resource("childOne.txt",
|
||||
"<displayname>childOne.txt</displayname>"
|
||||
),
|
||||
),
|
||||
"$PATH_WEBDAV_ROOT/parent2" to arrayOf(
|
||||
Resource("childTwo.txt",
|
||||
"<displayname>childTwo.txt</displayname>"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val responses = propsMap[requestPath]?.joinToString { resource ->
|
||||
"<response><href>$requestPath/${resource.name}</href><propstat><prop>" +
|
||||
resource.props +
|
||||
"</prop></propstat></response>"
|
||||
}
|
||||
|
||||
val multistatus =
|
||||
"<multistatus xmlns='DAV:' " +
|
||||
"xmlns:CARD='urn:ietf:params:xml:ns:carddav' " +
|
||||
"xmlns:CAL='urn:ietf:params:xml:ns:caldav'>" +
|
||||
responses +
|
||||
"</multistatus>"
|
||||
|
||||
Logger.log.info("Query path: $requestPath")
|
||||
Logger.log.info("Response: $multistatus")
|
||||
return MockResponse()
|
||||
.setResponseCode(207)
|
||||
.setBody(multistatus)
|
||||
}
|
||||
|
||||
return MockResponse().setResponseCode(404)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (c) Ricki Hirner (bitfire web engineering).
|
||||
~ All rights reserved. This program and the accompanying materials
|
||||
~ are made available under the terms of the GNU Public License v3.0
|
||||
~ which accompanies this distribution, and is available at
|
||||
~ http://www.gnu.org/licenses/gpl.html
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<string name="app_name">Davx5Test</string>
|
||||
|
||||
</resources>
|
||||
@@ -1,3 +0,0 @@
|
||||
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:accountType="at.bitfire.davdroid.SyncManagerTest"
|
||||
android:label="Test Account" />
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import okhttp3.Request
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
class OkhttpClientTest {
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Inject
|
||||
lateinit var settingsManager: SettingsManager
|
||||
|
||||
@Before
|
||||
fun inject() {
|
||||
hiltRule.inject()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testIcloudWithSettings() {
|
||||
val client = HttpClient.Builder(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||
.build()
|
||||
client.okHttpClient.newCall(Request.Builder()
|
||||
.get()
|
||||
.url("https://icloud.com")
|
||||
.build())
|
||||
.execute()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.os.Bundle
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
class AccountUtilsTest {
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Inject
|
||||
lateinit var settingsManager: SettingsManager
|
||||
|
||||
@Before
|
||||
fun inject() {
|
||||
hiltRule.inject()
|
||||
}
|
||||
|
||||
|
||||
val context by lazy { InstrumentationRegistry.getInstrumentation().targetContext }
|
||||
val account by lazy { Account("Test Account", context.getString(R.string.account_type)) }
|
||||
|
||||
@Test
|
||||
fun testCreateAccount() {
|
||||
val userData = Bundle(2)
|
||||
userData.putString("int", "1")
|
||||
userData.putString("string", "abc/\"-")
|
||||
try {
|
||||
assertTrue(AccountUtils.createAccount(context, account, userData))
|
||||
|
||||
// validate user data
|
||||
val manager = AccountManager.get(context)
|
||||
assertEquals("1", manager.getUserData(account, "int"))
|
||||
assertEquals("abc/\"-", manager.getUserData(account, "string"))
|
||||
} finally {
|
||||
val futureResult = AccountManager.get(context).removeAccount(account, {}, null)
|
||||
assertTrue(futureResult.getResult(10, TimeUnit.SECONDS))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.ContentResolver
|
||||
import android.net.wifi.WifiInfo
|
||||
import android.net.wifi.WifiManager
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.ContactsContract
|
||||
import android.util.Log
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.testing.WorkManagerTestInitHelper
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import at.bitfire.davdroid.network.ConnectionUtils
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.mockk.every
|
||||
import io.mockk.junit4.MockKRule
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.spyk
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class BaseSyncWorkerTest {
|
||||
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
private val accountManager = AccountManager.get(context)
|
||||
private val account = Account("Test Account", context.getString(R.string.account_type))
|
||||
private val fakeCredentials = Credentials("test", "test")
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
@get:Rule
|
||||
val mockkRule = MockKRule(this)
|
||||
|
||||
@Before
|
||||
fun inject() = hiltRule.inject()
|
||||
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
assertTrue(AccountUtils.createAccount(context, account, AccountSettings.initialUserData(fakeCredentials)))
|
||||
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1)
|
||||
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 0)
|
||||
|
||||
// The test application is an instance of HiltTestApplication, which doesn't initialize notification channels.
|
||||
// However, we need notification channels for the ongoing work notifications.
|
||||
NotificationUtils.createChannels(context)
|
||||
|
||||
// Initialize WorkManager for instrumentation tests.
|
||||
val config = Configuration.Builder()
|
||||
.setMinimumLoggingLevel(Log.DEBUG)
|
||||
.build()
|
||||
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
|
||||
}
|
||||
|
||||
@After
|
||||
fun removeAccount() {
|
||||
accountManager.removeAccountExplicitly(account)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testWifiConditionsMet_withoutWifi() {
|
||||
val accountSettings = mockk<AccountSettings>()
|
||||
every { accountSettings.getSyncWifiOnly() } returns false
|
||||
assertTrue(BaseSyncWorker.wifiConditionsMet(context, accountSettings))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWifiConditionsMet_anyWifi_wifiEnabled() {
|
||||
val accountSettings = AccountSettings(context, account)
|
||||
accountSettings.setSyncWiFiOnly(true)
|
||||
|
||||
mockkObject(ConnectionUtils)
|
||||
every { ConnectionUtils.wifiAvailable(any()) } returns true
|
||||
mockkObject(BaseSyncWorker.Companion)
|
||||
every { BaseSyncWorker.correctWifiSsid(any(), any()) } returns true
|
||||
|
||||
assertTrue(BaseSyncWorker.wifiConditionsMet(context, accountSettings))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWifiConditionsMet_anyWifi_wifiDisabled() {
|
||||
val accountSettings = AccountSettings(context, account)
|
||||
accountSettings.setSyncWiFiOnly(true)
|
||||
|
||||
mockkObject(ConnectionUtils)
|
||||
every { ConnectionUtils.wifiAvailable(any()) } returns false
|
||||
mockkObject(BaseSyncWorker.Companion)
|
||||
every { BaseSyncWorker.correctWifiSsid(any(), any()) } returns true
|
||||
|
||||
assertFalse(BaseSyncWorker.wifiConditionsMet(context, accountSettings))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testCorrectWifiSsid_CorrectWiFiSsid() {
|
||||
val accountSettings = AccountSettings(context, account)
|
||||
mockkObject(accountSettings)
|
||||
every { accountSettings.getSyncWifiOnlySSIDs() } returns listOf("SampleWiFi1","ConnectedWiFi")
|
||||
|
||||
mockkObject(PermissionUtils)
|
||||
every { PermissionUtils.canAccessWifiSsid(any()) } returns true
|
||||
|
||||
val wifiManager = context.getSystemService<WifiManager>()!!
|
||||
mockkObject(wifiManager)
|
||||
every { wifiManager.connectionInfo } returns spyk<WifiInfo>().apply {
|
||||
every { ssid } returns "ConnectedWiFi"
|
||||
}
|
||||
|
||||
assertTrue(BaseSyncWorker.correctWifiSsid(context, accountSettings))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCorrectWifiSsid_WrongWiFiSsid() {
|
||||
val accountSettings = AccountSettings(context, account)
|
||||
mockkObject(accountSettings)
|
||||
every { accountSettings.getSyncWifiOnlySSIDs() } returns listOf("SampleWiFi1","SampleWiFi2")
|
||||
|
||||
mockkObject(PermissionUtils)
|
||||
every { PermissionUtils.canAccessWifiSsid(any()) } returns true
|
||||
|
||||
val wifiManager = context.getSystemService<WifiManager>()!!
|
||||
mockkObject(wifiManager)
|
||||
every { wifiManager.connectionInfo } returns spyk<WifiInfo>().apply {
|
||||
every { ssid } returns "ConnectedWiFi"
|
||||
}
|
||||
|
||||
assertFalse(BaseSyncWorker.correctWifiSsid(context, accountSettings))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import at.bitfire.davdroid.db.SyncState
|
||||
import at.bitfire.davdroid.resource.LocalCollection
|
||||
|
||||
class LocalTestCollection: LocalCollection<LocalTestResource> {
|
||||
|
||||
override val tag = "LocalTestCollection"
|
||||
override val title = "Local Test Collection"
|
||||
|
||||
override var lastSyncState: SyncState? = null
|
||||
|
||||
val entries = mutableListOf<LocalTestResource>()
|
||||
|
||||
override val readOnly: Boolean
|
||||
get() = throw NotImplementedError()
|
||||
|
||||
override fun findDeleted() = entries.filter { it.deleted }
|
||||
override fun findDirty() = entries.filter { it.dirty }
|
||||
|
||||
override fun findByName(name: String) = entries.firstOrNull { it.fileName == name }
|
||||
|
||||
override fun markNotDirty(flags: Int): Int {
|
||||
var updated = 0
|
||||
for (dirty in findDirty()) {
|
||||
dirty.flags = flags
|
||||
updated++
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
override fun removeNotDirtyMarked(flags: Int): Int {
|
||||
val numBefore = entries.size
|
||||
entries.removeIf { !it.dirty && it.flags == flags }
|
||||
return numBefore - entries.size
|
||||
}
|
||||
|
||||
override fun forgetETags() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import at.bitfire.davdroid.resource.LocalResource
|
||||
|
||||
class LocalTestResource: LocalResource<Any> {
|
||||
|
||||
override val id: Long? = null
|
||||
override var fileName: String? = null
|
||||
override var eTag: String? = null
|
||||
override var scheduleTag: String? = null
|
||||
override var flags: Int = 0
|
||||
|
||||
var deleted = false
|
||||
var dirty = false
|
||||
|
||||
override fun prepareForUpload() = "generated-file.txt"
|
||||
|
||||
override fun clearDirty(fileName: String?, eTag: String?, scheduleTag: String?) {
|
||||
dirty = false
|
||||
if (fileName != null)
|
||||
this.fileName = fileName
|
||||
this.eTag = eTag
|
||||
this.scheduleTag = scheduleTag
|
||||
}
|
||||
|
||||
override fun updateFlags(flags: Int) {
|
||||
this.flags = flags
|
||||
}
|
||||
|
||||
override fun add() = throw NotImplementedError()
|
||||
override fun update(data: Any) = throw NotImplementedError()
|
||||
override fun delete() = throw NotImplementedError()
|
||||
override fun resetDeleted() = throw NotImplementedError()
|
||||
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.content.Context
|
||||
import android.provider.CalendarContract
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.davdroid.TestUtils
|
||||
import at.bitfire.davdroid.syncadapter.SyncManagerTest.Companion.account
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class OneTimeSyncWorkerTest {
|
||||
|
||||
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
@Test
|
||||
fun testEnqueue_enqueuesWorker() {
|
||||
OneTimeSyncWorker.enqueue(context, account, CalendarContract.AUTHORITY)
|
||||
val workerName = OneTimeSyncWorker.workerName(account, CalendarContract.AUTHORITY)
|
||||
Assert.assertTrue(TestUtils.workScheduledOrRunningOrSuccessful(context, workerName))
|
||||
}
|
||||
|
||||
}
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.ContactsContract
|
||||
import android.util.Log
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.ListenableWorker
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.testing.TestListenableWorkerBuilder
|
||||
import androidx.work.testing.WorkManagerTestInitHelper
|
||||
import androidx.work.workDataOf
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.TestUtils.workScheduledOrRunning
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.mockk.junit4.MockKRule
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.AfterClass
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class PeriodicSyncWorkerTest {
|
||||
|
||||
companion object {
|
||||
|
||||
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
private val accountManager = AccountManager.get(context)
|
||||
private val account = Account("Test Account", context.getString(R.string.account_type))
|
||||
private val fakeCredentials = Credentials("test", "test")
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun setUp() {
|
||||
// The test application is an instance of HiltTestApplication, which doesn't initialize notification channels.
|
||||
// However, we need notification channels for the ongoing work notifications.
|
||||
NotificationUtils.createChannels(context)
|
||||
|
||||
// Initialize WorkManager for instrumentation tests.
|
||||
val config = Configuration.Builder()
|
||||
.setMinimumLoggingLevel(Log.DEBUG)
|
||||
.build()
|
||||
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
|
||||
|
||||
assertTrue(AccountUtils.createAccount(context, account, AccountSettings.initialUserData(fakeCredentials)))
|
||||
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1)
|
||||
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1)
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@JvmStatic
|
||||
fun removeAccount() {
|
||||
accountManager.removeAccountExplicitly(account)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
@get:Rule
|
||||
val mockkRule = MockKRule(this)
|
||||
|
||||
@Before
|
||||
fun inject() {
|
||||
hiltRule.inject()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun enable_enqueuesPeriodicWorker() {
|
||||
PeriodicSyncWorker.enable(context, account, CalendarContract.AUTHORITY, 60, false)
|
||||
val workerName = PeriodicSyncWorker.workerName(account, CalendarContract.AUTHORITY)
|
||||
assertTrue(workScheduledOrRunning(context, workerName))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun disable_removesPeriodicWorker() {
|
||||
PeriodicSyncWorker.enable(context, account, CalendarContract.AUTHORITY, 60, false)
|
||||
PeriodicSyncWorker.disable(context, account, CalendarContract.AUTHORITY)
|
||||
val workerName = PeriodicSyncWorker.workerName(account, CalendarContract.AUTHORITY)
|
||||
assertFalse(workScheduledOrRunning(context, workerName))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun doWork_cancelsItselfOnInvalidAccount() {
|
||||
val invalidAccount = Account("invalid", context.getString(R.string.account_type))
|
||||
|
||||
// Run PeriodicSyncWorker as TestWorker
|
||||
val inputData = workDataOf(
|
||||
BaseSyncWorker.INPUT_AUTHORITY to CalendarContract.AUTHORITY,
|
||||
BaseSyncWorker.INPUT_ACCOUNT_NAME to invalidAccount.name,
|
||||
BaseSyncWorker.INPUT_ACCOUNT_TYPE to invalidAccount.type
|
||||
)
|
||||
|
||||
// mock WorkManager to observe cancellation call
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
mockkObject(workManager)
|
||||
|
||||
// run test worker, expect failure
|
||||
val testWorker = TestListenableWorkerBuilder<PeriodicSyncWorker>(context, inputData).build()
|
||||
val result = runBlocking {
|
||||
testWorker.doWork()
|
||||
}
|
||||
assertTrue(result is ListenableWorker.Result.Failure)
|
||||
|
||||
// verify that worker called WorkManager.cancelWorkById(<its ID>)
|
||||
verify {
|
||||
workManager.cancelWorkById(testWorker.id)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,530 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.testing.WorkManagerTestInitHelper
|
||||
import at.bitfire.dav4jvm.PropStat
|
||||
import at.bitfire.dav4jvm.Response
|
||||
import at.bitfire.dav4jvm.Response.HrefRelation
|
||||
import at.bitfire.dav4jvm.property.webdav.GetETag
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.TestUtils.assertWithin
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import at.bitfire.davdroid.db.SyncState
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.internal.http.StatusLine
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.After
|
||||
import org.junit.AfterClass
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
class SyncManagerTest {
|
||||
|
||||
companion object {
|
||||
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
val account = Account("SyncManagerTest", context.getString(R.string.account_type))
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun createAccount() {
|
||||
assertTrue(AccountManager.get(context).addAccountExplicitly(account, "test", AccountSettings.initialUserData(Credentials("test", "test"))))
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@JvmStatic
|
||||
fun removeAccount() {
|
||||
assertTrue(AccountManager.get(context).removeAccount(account, null, null).getResult(10, TimeUnit.SECONDS))
|
||||
|
||||
// clear annoying syncError notifications
|
||||
NotificationManagerCompat.from(context).cancelAll()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
@Inject
|
||||
lateinit var workerFactory: HiltWorkerFactory
|
||||
|
||||
val server = MockWebServer()
|
||||
|
||||
@Before
|
||||
fun inject() {
|
||||
hiltRule.inject()
|
||||
|
||||
// The test application is an instance of HiltTestApplication, which doesn't initialize notification channels.
|
||||
// However, we need notification channels for the ongoing work notifications.
|
||||
NotificationUtils.createChannels(context)
|
||||
|
||||
// Initialize WorkManager for instrumentation tests.
|
||||
val config = Configuration.Builder()
|
||||
.setMinimumLoggingLevel(Log.DEBUG)
|
||||
.setWorkerFactory(workerFactory)
|
||||
.build()
|
||||
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
|
||||
}
|
||||
|
||||
|
||||
private fun syncManager(collection: LocalTestCollection, syncResult: SyncResult = SyncResult()) =
|
||||
TestSyncManager(
|
||||
context,
|
||||
account,
|
||||
arrayOf(),
|
||||
"TestAuthority",
|
||||
HttpClient.Builder(InstrumentationRegistry.getInstrumentation().targetContext).build(),
|
||||
syncResult,
|
||||
collection,
|
||||
server
|
||||
)
|
||||
|
||||
@Before
|
||||
fun startServer() {
|
||||
server.start()
|
||||
}
|
||||
|
||||
@After
|
||||
fun stopServer() {
|
||||
server.close()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testGetDelayUntil_defaultOnNull() {
|
||||
val now = Instant.now()
|
||||
val delayUntil = SyncManager.getDelayUntil(null).epochSecond
|
||||
val default = now.plusSeconds(SyncManager.DELAY_UNTIL_DEFAULT).epochSecond
|
||||
assertWithin(default, delayUntil, 5)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetDelayUntil_reducesToMax() {
|
||||
val now = Instant.now()
|
||||
val delayUntil = SyncManager.getDelayUntil(now.plusSeconds(10*24*60*60)).epochSecond
|
||||
val max = now.plusSeconds(SyncManager.DELAY_UNTIL_MAX).epochSecond
|
||||
assertWithin(max, delayUntil, 5)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetDelayUntil_increasesToMin() {
|
||||
val delayUntil = SyncManager.getDelayUntil(Instant.EPOCH).epochSecond
|
||||
val min = Instant.now().plusSeconds(SyncManager.DELAY_UNTIL_MIN).epochSecond
|
||||
assertWithin(min, delayUntil, 5)
|
||||
}
|
||||
|
||||
|
||||
private fun queryCapabilitiesResponse(cTag: String? = null): MockResponse {
|
||||
val body = StringBuilder()
|
||||
body.append(
|
||||
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
|
||||
"<multistatus xmlns=\"DAV:\" xmlns:CALDAV=\"http://calendarserver.org/ns/\">\n" +
|
||||
" <response>\n" +
|
||||
" <href>/</href>\n" +
|
||||
" <propstat>\n" +
|
||||
" <prop>\n"
|
||||
)
|
||||
if (cTag != null)
|
||||
body.append("<CALDAV:getctag>$cTag</CALDAV:getctag>\n")
|
||||
body.append(
|
||||
" </prop>\n" +
|
||||
" </propstat>\n" +
|
||||
" </response>\n" +
|
||||
"</multistatus>"
|
||||
)
|
||||
return MockResponse()
|
||||
.setResponseCode(207)
|
||||
.setHeader("Content-Type", "text/xml")
|
||||
.setBody(body.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPerformSync_503RetryAfter_DelaySeconds() {
|
||||
server.enqueue(MockResponse()
|
||||
.setResponseCode(503)
|
||||
.setHeader("Retry-After", "60")) // 60 seconds
|
||||
|
||||
val result = SyncResult()
|
||||
val syncManager = syncManager(LocalTestCollection(), result)
|
||||
syncManager.performSync()
|
||||
|
||||
val expected = Instant.now()
|
||||
.plusSeconds(60)
|
||||
.toEpochMilli()
|
||||
// 5 sec tolerance for test
|
||||
assertWithin(expected, result.delayUntil*1000, 5000)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPerformSync_FirstSync_Empty() {
|
||||
val collection = LocalTestCollection() /* no last known ctag */
|
||||
server.enqueue(queryCapabilitiesResponse())
|
||||
|
||||
val syncManager = syncManager(collection)
|
||||
syncManager.performSync()
|
||||
|
||||
assertFalse(syncManager.didGenerateUpload)
|
||||
assertTrue(syncManager.didListAllRemote)
|
||||
assertFalse(syncManager.didDownloadRemote)
|
||||
assertFalse(syncManager.syncResult.hasError())
|
||||
assertTrue(collection.entries.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPerformSync_UploadNewMember_ETagOnPut() {
|
||||
val collection = LocalTestCollection().apply {
|
||||
lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag")
|
||||
entries += LocalTestResource().apply {
|
||||
dirty = true
|
||||
}
|
||||
}
|
||||
server.enqueue(queryCapabilitiesResponse("ctag1"))
|
||||
|
||||
// PUT -> 204 No Content
|
||||
server.enqueue(MockResponse()
|
||||
.setResponseCode(204)
|
||||
.setHeader("ETag", "etag-from-put"))
|
||||
|
||||
// modifications sent, so DAVx5 will query CTag again
|
||||
server.enqueue(queryCapabilitiesResponse("ctag2"))
|
||||
|
||||
val syncManager = syncManager(collection).apply {
|
||||
listAllRemoteResult = listOf(
|
||||
Pair(Response(
|
||||
server.url("/"),
|
||||
server.url("/generated-file.txt"),
|
||||
null,
|
||||
listOf(PropStat(
|
||||
listOf(
|
||||
GetETag("\"etag-from-put\"")
|
||||
),
|
||||
StatusLine(Protocol.HTTP_1_1, 200, "OK")
|
||||
)
|
||||
)), HrefRelation.MEMBER)
|
||||
)
|
||||
}
|
||||
syncManager.performSync()
|
||||
|
||||
assertTrue(syncManager.didGenerateUpload)
|
||||
assertTrue(syncManager.didListAllRemote)
|
||||
assertFalse(syncManager.didDownloadRemote)
|
||||
assertFalse(syncManager.syncResult.hasError())
|
||||
assertEquals(1, collection.entries.size)
|
||||
assertEquals("etag-from-put", collection.entries.first().eTag)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPerformSync_UploadModifiedMember_ETagOnPut() {
|
||||
val collection = LocalTestCollection().apply {
|
||||
lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag")
|
||||
entries += LocalTestResource().apply {
|
||||
fileName = "existing-file.txt"
|
||||
eTag = "old-etag-like-on-server"
|
||||
dirty = true
|
||||
}
|
||||
}
|
||||
server.enqueue(queryCapabilitiesResponse("ctag1"))
|
||||
|
||||
// PUT -> 204 No Content
|
||||
server.enqueue(MockResponse()
|
||||
.setResponseCode(204)
|
||||
.addHeader("ETag", "etag-from-put"))
|
||||
|
||||
// modifications sent, so DAVx5 will query CTag again
|
||||
server.enqueue(queryCapabilitiesResponse("ctag2"))
|
||||
|
||||
val syncManager = syncManager(collection).apply {
|
||||
listAllRemoteResult = listOf(
|
||||
Pair(Response(
|
||||
server.url("/"),
|
||||
server.url("/existing-file.txt"),
|
||||
null,
|
||||
listOf(PropStat(
|
||||
listOf(
|
||||
GetETag("etag-from-put")
|
||||
),
|
||||
StatusLine(Protocol.HTTP_1_1, 200, "OK")
|
||||
)
|
||||
)), HrefRelation.MEMBER)
|
||||
)
|
||||
|
||||
assertDownloadRemote = mapOf(Pair(server.url("/existing-file.txt"), "etag-from-put"))
|
||||
}
|
||||
syncManager.performSync()
|
||||
|
||||
assertTrue(syncManager.didGenerateUpload)
|
||||
assertTrue(syncManager.didListAllRemote)
|
||||
assertFalse(syncManager.didDownloadRemote)
|
||||
assertFalse(syncManager.syncResult.hasError())
|
||||
assertEquals(1, collection.entries.size)
|
||||
assertEquals("etag-from-put", collection.entries.first().eTag)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPerformSync_UploadModifiedMember_NoETagOnPut() {
|
||||
val collection = LocalTestCollection().apply {
|
||||
lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag")
|
||||
entries += LocalTestResource().apply {
|
||||
fileName = "existing-file.txt"
|
||||
eTag = "old-etag-like-on-server"
|
||||
dirty = true
|
||||
}
|
||||
}
|
||||
server.enqueue(queryCapabilitiesResponse("ctag1"))
|
||||
|
||||
// PUT -> 204 No Content
|
||||
server.enqueue(MockResponse().setResponseCode(204))
|
||||
|
||||
// modifications sent, so DAVx5 will query CTag again
|
||||
server.enqueue(queryCapabilitiesResponse("ctag2"))
|
||||
|
||||
val syncManager = syncManager(collection).apply {
|
||||
listAllRemoteResult = listOf(
|
||||
Pair(Response(
|
||||
server.url("/"),
|
||||
server.url("/existing-file.txt"),
|
||||
null,
|
||||
listOf(PropStat(
|
||||
listOf(
|
||||
GetETag("etag-from-propfind")
|
||||
),
|
||||
StatusLine(Protocol.HTTP_1_1, 200, "OK")
|
||||
)
|
||||
)), HrefRelation.MEMBER)
|
||||
)
|
||||
|
||||
assertDownloadRemote = mapOf(Pair(server.url("/existing-file.txt"), "etag-from-propfind"))
|
||||
}
|
||||
syncManager.performSync()
|
||||
|
||||
assertTrue(syncManager.didGenerateUpload)
|
||||
assertTrue(syncManager.didListAllRemote)
|
||||
assertTrue(syncManager.didDownloadRemote)
|
||||
assertFalse(syncManager.syncResult.hasError())
|
||||
assertEquals(1, collection.entries.size)
|
||||
assertEquals("etag-from-propfind", collection.entries.first().eTag)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPerformSync_UploadModifiedMember_412PreconditionFailed() {
|
||||
val collection = LocalTestCollection().apply {
|
||||
lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag")
|
||||
entries += LocalTestResource().apply {
|
||||
fileName = "existing-file.txt"
|
||||
eTag = "etag-that-has-been-changed-on-server-in-the-meanwhile"
|
||||
dirty = true
|
||||
}
|
||||
}
|
||||
server.enqueue(queryCapabilitiesResponse("ctag1"))
|
||||
|
||||
// PUT -> 412 Precondition Failed
|
||||
server.enqueue(MockResponse()
|
||||
.setResponseCode(412))
|
||||
|
||||
// modifications sent, so DAVx5 will query CTag again
|
||||
server.enqueue(queryCapabilitiesResponse("ctag1"))
|
||||
|
||||
val syncManager = syncManager(collection).apply {
|
||||
listAllRemoteResult = listOf(
|
||||
Pair(Response(
|
||||
server.url("/"),
|
||||
server.url("/existing-file.txt"),
|
||||
null,
|
||||
listOf(PropStat(
|
||||
listOf(
|
||||
GetETag("changed-etag-from-server")
|
||||
),
|
||||
StatusLine(Protocol.HTTP_1_1, 200, "OK")
|
||||
)
|
||||
)), HrefRelation.MEMBER)
|
||||
)
|
||||
|
||||
assertDownloadRemote = mapOf(Pair(server.url("/existing-file.txt"), "changed-etag-from-server"))
|
||||
}
|
||||
syncManager.performSync()
|
||||
|
||||
assertTrue(syncManager.didGenerateUpload)
|
||||
assertTrue(syncManager.didListAllRemote)
|
||||
assertTrue(syncManager.didDownloadRemote)
|
||||
assertFalse(syncManager.syncResult.hasError())
|
||||
assertEquals(1, collection.entries.size)
|
||||
assertEquals("changed-etag-from-server", collection.entries.first().eTag)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPerformSync_NoopOnMemberWithSameETag() {
|
||||
val collection = LocalTestCollection().apply {
|
||||
lastSyncState = SyncState(SyncState.Type.CTAG, "ctag1")
|
||||
entries += LocalTestResource().apply {
|
||||
fileName = "downloaded-member.txt"
|
||||
eTag = "MemberETag1"
|
||||
}
|
||||
}
|
||||
server.enqueue(queryCapabilitiesResponse("ctag2"))
|
||||
|
||||
val syncManager = syncManager(collection).apply {
|
||||
listAllRemoteResult = listOf(
|
||||
Pair(Response(
|
||||
server.url("/"),
|
||||
server.url("/downloaded-member.txt"),
|
||||
null,
|
||||
listOf(PropStat(
|
||||
listOf(
|
||||
GetETag("\"MemberETag1\"")
|
||||
),
|
||||
StatusLine(Protocol.HTTP_1_1, 200, "OK")
|
||||
)
|
||||
)), HrefRelation.MEMBER)
|
||||
)
|
||||
|
||||
}
|
||||
syncManager.performSync()
|
||||
|
||||
assertFalse(syncManager.didGenerateUpload)
|
||||
assertTrue(syncManager.didListAllRemote)
|
||||
assertFalse(syncManager.didDownloadRemote)
|
||||
assertFalse(syncManager.syncResult.hasError())
|
||||
assertEquals(1, collection.entries.size)
|
||||
assertEquals("MemberETag1", collection.entries.first().eTag)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPerformSync_DownloadNewMember() {
|
||||
val collection = LocalTestCollection().apply {
|
||||
lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag")
|
||||
}
|
||||
server.enqueue(queryCapabilitiesResponse(cTag = "new-ctag"))
|
||||
|
||||
val syncManager = syncManager(collection).apply {
|
||||
listAllRemoteResult = listOf(
|
||||
Pair(Response(
|
||||
server.url("/"),
|
||||
server.url("/new-member.txt"),
|
||||
null,
|
||||
listOf(PropStat(
|
||||
listOf(
|
||||
GetETag("\"NewMemberETag1\"")
|
||||
),
|
||||
StatusLine(Protocol.HTTP_1_1, 200, "OK")
|
||||
)
|
||||
)), HrefRelation.MEMBER)
|
||||
)
|
||||
|
||||
assertDownloadRemote = mapOf(Pair(server.url("/new-member.txt"), "NewMemberETag1"))
|
||||
}
|
||||
syncManager.performSync()
|
||||
|
||||
assertFalse(syncManager.didGenerateUpload)
|
||||
assertTrue(syncManager.didListAllRemote)
|
||||
assertTrue(syncManager.didDownloadRemote)
|
||||
assertFalse(syncManager.syncResult.hasError())
|
||||
assertEquals(1, collection.entries.size)
|
||||
assertEquals("NewMemberETag1", collection.entries.first().eTag)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPerformSync_DownloadUpdatedMember() {
|
||||
val collection = LocalTestCollection().apply {
|
||||
lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag")
|
||||
entries += LocalTestResource().apply {
|
||||
fileName = "downloaded-member.txt"
|
||||
eTag = "MemberETag1"
|
||||
}
|
||||
}
|
||||
server.enqueue(queryCapabilitiesResponse(cTag = "new-ctag"))
|
||||
|
||||
val syncManager = syncManager(collection).apply {
|
||||
listAllRemoteResult = listOf(
|
||||
Pair(Response(
|
||||
server.url("/"),
|
||||
server.url("/downloaded-member.txt"),
|
||||
null,
|
||||
listOf(PropStat(
|
||||
listOf(
|
||||
GetETag("\"MemberETag2\"")
|
||||
),
|
||||
StatusLine(Protocol.HTTP_1_1, 200, "OK")
|
||||
)
|
||||
)), HrefRelation.MEMBER)
|
||||
)
|
||||
|
||||
assertDownloadRemote = mapOf(Pair(server.url("/downloaded-member.txt"), "MemberETag2"))
|
||||
}
|
||||
syncManager.performSync()
|
||||
|
||||
assertFalse(syncManager.didGenerateUpload)
|
||||
assertTrue(syncManager.didListAllRemote)
|
||||
assertTrue(syncManager.didDownloadRemote)
|
||||
assertFalse(syncManager.syncResult.hasError())
|
||||
assertEquals(1, collection.entries.size)
|
||||
assertEquals("MemberETag2", collection.entries.first().eTag)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPerformSync_RemoveVanishedMember() {
|
||||
val collection = LocalTestCollection().apply {
|
||||
lastSyncState = SyncState(SyncState.Type.CTAG, "old-ctag")
|
||||
entries += LocalTestResource().apply {
|
||||
fileName = "downloaded-member.txt"
|
||||
}
|
||||
}
|
||||
server.enqueue(queryCapabilitiesResponse(cTag = "new-ctag"))
|
||||
|
||||
val syncManager = syncManager(collection)
|
||||
syncManager.performSync()
|
||||
|
||||
assertFalse(syncManager.didGenerateUpload)
|
||||
assertTrue(syncManager.didListAllRemote)
|
||||
assertFalse(syncManager.didDownloadRemote)
|
||||
assertFalse(syncManager.syncResult.hasError())
|
||||
assertTrue(collection.entries.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPerformSync_CTagDidntChange() {
|
||||
val collection = LocalTestCollection().apply {
|
||||
lastSyncState = SyncState(SyncState.Type.CTAG, "ctag1")
|
||||
}
|
||||
server.enqueue(queryCapabilitiesResponse("ctag1"))
|
||||
|
||||
val syncManager = syncManager(collection)
|
||||
syncManager.performSync()
|
||||
|
||||
assertFalse(syncManager.didGenerateUpload)
|
||||
assertFalse(syncManager.didListAllRemote)
|
||||
assertFalse(syncManager.didDownloadRemote)
|
||||
assertFalse(syncManager.syncResult.hasError())
|
||||
assertTrue(collection.entries.isEmpty())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.R
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
@HiltAndroidTest
|
||||
class SyncerTest {
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
/** use our WebDAV provider as a mock provider because it's our own and we don't need any permissions for it */
|
||||
val mockAuthority = context.getString(R.string.webdav_authority)
|
||||
val mockProvider = context.contentResolver!!.acquireContentProviderClient(mockAuthority)!!
|
||||
|
||||
val account = Account(javaClass.canonicalName, context.getString(R.string.account_type))
|
||||
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
hiltRule.inject()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnPerformSync_runsSyncAndSetsClassLoader() {
|
||||
val syncer = TestSyncer(context)
|
||||
syncer.onPerformSync(account, arrayOf(), mockAuthority, mockProvider, SyncResult())
|
||||
|
||||
// check whether onPerformSync() actually calls sync()
|
||||
assertEquals(1, syncer.syncCalled.get())
|
||||
|
||||
// check whether contextClassLoader is set
|
||||
assertEquals(context.classLoader, Thread.currentThread().contextClassLoader)
|
||||
}
|
||||
|
||||
|
||||
class TestSyncer(context: Context) : Syncer(context) {
|
||||
|
||||
val syncCalled = AtomicInteger()
|
||||
|
||||
override fun sync(
|
||||
account: Account,
|
||||
extras: Array<String>,
|
||||
authority: String,
|
||||
httpClient: Lazy<HttpClient>,
|
||||
provider: ContentProviderClient,
|
||||
syncResult: SyncResult
|
||||
) {
|
||||
Thread.sleep(1000)
|
||||
syncCalled.incrementAndGet()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import at.bitfire.dav4jvm.DavCollection
|
||||
import at.bitfire.dav4jvm.MultiResponseCallback
|
||||
import at.bitfire.dav4jvm.Response
|
||||
import at.bitfire.dav4jvm.property.caldav.GetCTag
|
||||
import at.bitfire.davdroid.db.SyncState
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.resource.LocalResource
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.Assert.assertEquals
|
||||
|
||||
class TestSyncManager(
|
||||
context: Context,
|
||||
account: Account,
|
||||
extras: Array<String>,
|
||||
authority: String,
|
||||
httpClient: HttpClient,
|
||||
syncResult: SyncResult,
|
||||
localCollection: LocalTestCollection,
|
||||
val mockWebServer: MockWebServer
|
||||
): SyncManager<LocalTestResource, LocalTestCollection, DavCollection>(context, account, AccountSettings(context, account), httpClient, extras, authority, syncResult, localCollection) {
|
||||
|
||||
override fun prepare(): Boolean {
|
||||
collectionURL = mockWebServer.url("/")
|
||||
davCollection = DavCollection(httpClient.okHttpClient, collectionURL)
|
||||
return true
|
||||
}
|
||||
|
||||
var didQueryCapabilities = false
|
||||
override fun queryCapabilities(): SyncState? {
|
||||
if (didQueryCapabilities)
|
||||
throw IllegalStateException("queryCapabilities() must not be called twice")
|
||||
didQueryCapabilities = true
|
||||
|
||||
var cTag: SyncState? = null
|
||||
davCollection.propfind(0, GetCTag.NAME) { response, rel ->
|
||||
if (rel == Response.HrefRelation.SELF)
|
||||
response[GetCTag::class.java]?.cTag?.let {
|
||||
cTag = SyncState(SyncState.Type.CTAG, it)
|
||||
}
|
||||
}
|
||||
|
||||
return cTag
|
||||
}
|
||||
|
||||
var didGenerateUpload = false
|
||||
override fun generateUpload(resource: LocalTestResource): RequestBody {
|
||||
didGenerateUpload = true
|
||||
return resource.toString().toRequestBody()
|
||||
}
|
||||
|
||||
override fun syncAlgorithm() = SyncAlgorithm.PROPFIND_REPORT
|
||||
|
||||
var listAllRemoteResult = emptyList<Pair<Response, Response.HrefRelation>>()
|
||||
var didListAllRemote = false
|
||||
override fun listAllRemote(callback: MultiResponseCallback) {
|
||||
if (didListAllRemote)
|
||||
throw IllegalStateException("listAllRemote() must not be called twice")
|
||||
didListAllRemote = true
|
||||
for (result in listAllRemoteResult)
|
||||
callback.onResponse(result.first, result.second)
|
||||
}
|
||||
|
||||
var assertDownloadRemote = emptyMap<HttpUrl, String>()
|
||||
var didDownloadRemote = false
|
||||
override fun downloadRemote(bunch: List<HttpUrl>) {
|
||||
didDownloadRemote = true
|
||||
assertEquals(assertDownloadRemote.keys.toList(), bunch)
|
||||
|
||||
for ((url, eTag) in assertDownloadRemote) {
|
||||
val fileName = DavUtils.lastSegmentOfUrl(url)
|
||||
var localEntry = localCollection.entries.firstOrNull { it.fileName == fileName }
|
||||
if (localEntry == null) {
|
||||
val newEntry = LocalTestResource().also {
|
||||
it.fileName = fileName
|
||||
}
|
||||
localCollection.entries += newEntry
|
||||
localEntry = newEntry
|
||||
}
|
||||
localEntry.eTag = eTag
|
||||
localEntry.flags = LocalResource.FLAG_REMOTELY_PRESENT
|
||||
}
|
||||
}
|
||||
|
||||
override fun postProcess() {
|
||||
}
|
||||
|
||||
override fun notifyInvalidResourceTitle() =
|
||||
throw NotImplementedError()
|
||||
|
||||
}
|
||||
@@ -1,339 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:installLocation="internalOnly">
|
||||
|
||||
<!-- normal permissions -->
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.READ_SYNC_STATS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
|
||||
<!-- account management permissions not required for own accounts since API level 22 -->
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" android:maxSdkVersion="22"/>
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" android:maxSdkVersion="22"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" android:maxSdkVersion="22"/>
|
||||
|
||||
<!-- other permissions -->
|
||||
<!-- android.permission-group.CONTACTS -->
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
<!-- android.permission-group.CALENDAR -->
|
||||
<uses-permission android:name="android.permission.READ_CALENDAR"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
|
||||
|
||||
<!-- android.permission-group.LOCATION -->
|
||||
<!-- getting the WiFi name (for "sync in Wifi only") requires
|
||||
- coarse location (Android 8.1)
|
||||
- fine location (Android 10) -->
|
||||
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<!-- required since Android 10 to get the WiFi name while in background (= while syncing) -->
|
||||
<uses-permission-sdk-23 android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
|
||||
|
||||
<!-- ical4android declares task access permissions -->
|
||||
|
||||
<!-- Disable GPS capability requirement, which is implicitly derived from ACCESS_FINE_LOCATION
|
||||
permission and makes app unusable on some devices without GPS. We need location permissions only
|
||||
to get the current WiFi SSID, and we don't need GPS for that. -->
|
||||
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="false"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme"
|
||||
android:resizeableActivity="true"
|
||||
tools:ignore="UnusedAttribute"
|
||||
android:supportsRtl="true">
|
||||
|
||||
<!-- required for Hilt/WorkManager integration -->
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
tools:node="remove">
|
||||
</provider>
|
||||
|
||||
<!-- Remove the node added by AppAuth (remove only from net.openid.appauth library, not from our flavor manifest files) -->
|
||||
<activity android:name="net.openid.appauth.RedirectUriReceiverActivity"
|
||||
tools:node="remove" tools:selector="net.openid.appauth"/>
|
||||
|
||||
<activity android:name=".ui.intro.IntroActivity" />
|
||||
<activity
|
||||
android:name=".ui.AccountsActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.AboutActivity"
|
||||
android:label="@string/navigation_drawer_about"
|
||||
android:parentActivityName=".ui.AccountsActivity"/>
|
||||
|
||||
<activity
|
||||
android:name=".ui.AppSettingsActivity"
|
||||
android:label="@string/app_settings"
|
||||
android:parentActivityName=".ui.AccountsActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.APPLICATION_PREFERENCES"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.DebugInfoActivity"
|
||||
android:parentActivityName=".ui.AppSettingsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/debug_info_title">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BUG_REPORT"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.PermissionsActivity"
|
||||
android:label="@string/app_settings_security_app_permissions"
|
||||
android:parentActivityName=".ui.AppSettingsActivity" />
|
||||
<activity
|
||||
android:name=".ui.TasksActivity"
|
||||
android:label="@string/intro_tasks_title"
|
||||
android:parentActivityName=".ui.AppSettingsActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.setup.LoginActivity"
|
||||
android:parentActivityName=".ui.AccountsActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:scheme="caldav"/>
|
||||
<data android:scheme="caldavs"/>
|
||||
<data android:scheme="carddav"/>
|
||||
<data android:scheme="carddavs"/>
|
||||
<data android:scheme="davx5"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="loginFlow" /> <!-- Ensures this filter matches, even if the sending app is not defining an action -->
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.account.AccountActivity"
|
||||
android:parentActivityName=".ui.AccountsActivity"
|
||||
android:exported="true">
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.account.CreateAddressBookActivity"
|
||||
android:parentActivityName=".ui.account.AccountActivity" />
|
||||
<activity
|
||||
android:name=".ui.account.CreateCalendarActivity"
|
||||
android:parentActivityName=".ui.account.AccountActivity" />
|
||||
<activity
|
||||
android:name=".ui.account.AccountSettingsActivity"
|
||||
android:parentActivityName=".ui.account.AccountActivity" />
|
||||
<activity
|
||||
android:name=".ui.account.WifiPermissionsActivity"
|
||||
android:parentActivityName=".ui.account.AccountSettingsActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.webdav.WebdavMountsActivity"
|
||||
android:exported="true"
|
||||
android:parentActivityName=".ui.AccountsActivity" />
|
||||
<activity
|
||||
android:name=".ui.webdav.AddWebdavMountActivity"
|
||||
android:parentActivityName=".ui.webdav.WebdavMountsActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
<!-- account type "DAVx⁵" -->
|
||||
<service
|
||||
android:name=".syncadapter.AccountAuthenticatorService"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/account_authenticator"/>
|
||||
</service>
|
||||
<service
|
||||
android:name=".syncadapter.CalendarsSyncAdapterService"
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedService">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_calendars"/>
|
||||
</service>
|
||||
<service
|
||||
android:name=".syncadapter.JtxSyncAdapterService"
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedService">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_notes"/>
|
||||
</service>
|
||||
<service
|
||||
android:name=".syncadapter.OpenTasksSyncAdapterService"
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedService">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_opentasks"/>
|
||||
</service>
|
||||
<service
|
||||
android:name=".syncadapter.TasksOrgSyncAdapterService"
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedService">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_tasks_org"/>
|
||||
</service>
|
||||
<provider
|
||||
android:authorities="@string/webdav_authority"
|
||||
android:name=".webdav.DavDocumentsProvider"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||
</intent-filter>
|
||||
</provider>
|
||||
|
||||
<!-- account type "DAVx⁵ Address book" -->
|
||||
<service
|
||||
android:name=".syncadapter.AddressBookAuthenticatorService"
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedService"> <!-- Since Android 11, this must be true so that Google Contacts shows the address book accounts -->
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/account_authenticator_address_book"/>
|
||||
</service>
|
||||
<provider
|
||||
android:authorities="@string/address_books_authority"
|
||||
android:exported="false"
|
||||
android:label="@string/address_books_authority_title"
|
||||
android:name=".syncadapter.AddressBookProvider" />
|
||||
<service
|
||||
android:name=".syncadapter.AddressBooksSyncAdapterService"
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedService">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_address_books"/>
|
||||
</service>
|
||||
<service
|
||||
android:name=".syncadapter.ContactsSyncAdapterService"
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedService">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter"/>
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_contacts"/>
|
||||
<meta-data
|
||||
android:name="android.provider.CONTACTS_STRUCTURE"
|
||||
android:resource="@xml/contacts"/>
|
||||
</service>
|
||||
|
||||
<!-- provider to share debug info/logs -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="@string/authority_debug_provider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/debug_paths" />
|
||||
</provider>
|
||||
|
||||
<!-- Widgets -->
|
||||
<receiver android:name=".ui.widget.SyncButtonWidgetReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_info_sync_button" />
|
||||
</receiver>
|
||||
|
||||
</application>
|
||||
|
||||
<!-- package visiblity – which apps do we need to see? -->
|
||||
<queries>
|
||||
<!-- system providers (listing them is technically not required, but some apps like the
|
||||
Huawei calendar take this as indication of whether these providers are accessed) -->
|
||||
<provider android:authorities="com.android.calendar"/>
|
||||
<provider android:authorities="com.android.contacts"/>
|
||||
|
||||
<!-- task providers -->
|
||||
<package android:name="at.techbee.jtx" />
|
||||
<package android:name="org.dmfs.tasks" />
|
||||
<package android:name="org.tasks" />
|
||||
|
||||
<!-- ICSx5 for Webcal feeds -->
|
||||
<package android:name="at.bitfire.icsdroid"/>
|
||||
|
||||
<!-- apps that interact with contact, calendar, task data (for debug info) -->
|
||||
<intent>
|
||||
<action android:name="*" />
|
||||
<data android:scheme="content" android:host="com.android.contacts" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="*" />
|
||||
<data android:scheme="content" android:host="com.android.calendar" />
|
||||
</intent>
|
||||
|
||||
<!-- Open URLs in a browser or other app [https://developer.android.com/training/package-visibility/use-cases#open-urls-browser-or-other-app] -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
|
||||
<!-- Custom Tabs support (e.g. Nextcloud Login Flow) -->
|
||||
<intent>
|
||||
<action android:name="android.support.customtabs.action.CustomTabsService" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
@@ -1,628 +0,0 @@
|
||||
<h3 style="text-align: center;">GNU GENERAL PUBLIC LICENSE</h3>
|
||||
<p style="text-align: center;">Version 3, 29 June 2007</p>
|
||||
|
||||
<p>Copyright © 2007 Free Software Foundation, Inc.
|
||||
<<a href="http://fsf.org/">http://fsf.org/</a>></p><p>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.</p>
|
||||
|
||||
<h3><a name="preamble"></a>Preamble</h3>
|
||||
|
||||
<p>The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.</p>
|
||||
|
||||
<p>The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.</p>
|
||||
|
||||
<p>When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.</p>
|
||||
|
||||
<p>To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.</p>
|
||||
|
||||
<p>For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.</p>
|
||||
|
||||
<p>Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.</p>
|
||||
|
||||
<p>For the developers\' and authors\' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users\' and
|
||||
authors\' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.</p>
|
||||
|
||||
<p>Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users\' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.</p>
|
||||
|
||||
<p>Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.</p>
|
||||
|
||||
<p>The precise terms and conditions for copying, distribution and
|
||||
modification follow.</p>
|
||||
|
||||
<h3><a name="terms"></a>TERMS AND CONDITIONS</h3>
|
||||
|
||||
<h4><a name="section0"></a>0. Definitions.</h4>
|
||||
|
||||
<p>“This License” refers to version 3 of the GNU General Public License.</p>
|
||||
|
||||
<p>“Copyright” also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.</p>
|
||||
|
||||
<p>“The Program” refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as “you”. “Licensees” and
|
||||
“recipients” may be individuals or organizations.</p>
|
||||
|
||||
<p>To “modify” a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a “modified version” of the
|
||||
earlier work or a work “based on” the earlier work.</p>
|
||||
|
||||
<p>A “covered work” means either the unmodified Program or a work based
|
||||
on the Program.</p>
|
||||
|
||||
<p>To “propagate” a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.</p>
|
||||
|
||||
<p>To “convey” a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.</p>
|
||||
|
||||
<p>An interactive user interface displays “Appropriate Legal Notices”
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.</p>
|
||||
|
||||
<h4><a name="section1"></a>1. Source Code.</h4>
|
||||
|
||||
<p>The “source code” for a work means the preferred form of the work
|
||||
for making modifications to it. “Object code” means any non-source
|
||||
form of a work.</p>
|
||||
|
||||
<p>A “Standard Interface” means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.</p>
|
||||
|
||||
<p>The “System Libraries” of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
“Major Component”, in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.</p>
|
||||
|
||||
<p>The “Corresponding Source” for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work\'s
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.</p>
|
||||
|
||||
<p>The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.</p>
|
||||
|
||||
<p>The Corresponding Source for a work in source code form is that
|
||||
same work.</p>
|
||||
|
||||
<h4><a name="section2"></a>2. Basic Permissions.</h4>
|
||||
|
||||
<p>All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.</p>
|
||||
|
||||
<p>You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.</p>
|
||||
|
||||
<p>Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.</p>
|
||||
|
||||
<h4><a name="section3"></a>3. Protecting Users\' Legal Rights From Anti-Circumvention Law.</h4>
|
||||
|
||||
<p>No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.</p>
|
||||
|
||||
<p>When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work\'s
|
||||
users, your or third parties\' legal rights to forbid circumvention of
|
||||
technological measures.</p>
|
||||
|
||||
<h4><a name="section4"></a>4. Conveying Verbatim Copies.</h4>
|
||||
|
||||
<p>You may convey verbatim copies of the Program\'s source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.</p>
|
||||
|
||||
<p>You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.</p>
|
||||
|
||||
<h4><a name="section5"></a>5. Conveying Modified Source Versions.</h4>
|
||||
|
||||
<p>You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:</p>
|
||||
|
||||
<ul>
|
||||
<li>a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.</li>
|
||||
|
||||
<li>b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
“keep intact all notices”.</li>
|
||||
|
||||
<li>c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.</li>
|
||||
|
||||
<li>d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.</li>
|
||||
</ul>
|
||||
|
||||
<p>A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
“aggregate” if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation\'s users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.</p>
|
||||
|
||||
<h4><a name="section6"></a>6. Conveying Non-Source Forms.</h4>
|
||||
|
||||
<p>You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:</p>
|
||||
|
||||
<ul>
|
||||
<li>a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.</li>
|
||||
|
||||
<li>b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.</li>
|
||||
|
||||
<li>c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.</li>
|
||||
|
||||
<li>d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.</li>
|
||||
|
||||
<li>e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.</li>
|
||||
</ul>
|
||||
|
||||
<p>A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.</p>
|
||||
|
||||
<p>A “User Product” is either (1) a “consumer product”, which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, “normally used” refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.</p>
|
||||
|
||||
<p>“Installation Information” for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.</p>
|
||||
|
||||
<p>If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).</p>
|
||||
|
||||
<p>The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.</p>
|
||||
|
||||
<p>Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.</p>
|
||||
|
||||
<h4><a name="section7"></a>7. Additional Terms.</h4>
|
||||
|
||||
<p>“Additional permissions” are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.</p>
|
||||
|
||||
<p>When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.</p>
|
||||
|
||||
<p>Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:</p>
|
||||
|
||||
<ul>
|
||||
<li>a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or</li>
|
||||
|
||||
<li>b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or</li>
|
||||
|
||||
<li>c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or</li>
|
||||
|
||||
<li>d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or</li>
|
||||
|
||||
<li>e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or</li>
|
||||
|
||||
<li>f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.</li>
|
||||
</ul>
|
||||
|
||||
<p>All other non-permissive additional terms are considered “further
|
||||
restrictions” within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.</p>
|
||||
|
||||
<p>If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.</p>
|
||||
|
||||
<p>Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.</p>
|
||||
|
||||
<h4><a name="section8"></a>8. Termination.</h4>
|
||||
|
||||
<p>You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).</p>
|
||||
|
||||
<p>However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.</p>
|
||||
|
||||
<p>Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.</p>
|
||||
|
||||
<p>Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.</p>
|
||||
|
||||
<h4><a name="section9"></a>9. Acceptance Not Required for Having Copies.</h4>
|
||||
|
||||
<p>You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.</p>
|
||||
|
||||
<h4><a name="section10"></a>10. Automatic Licensing of Downstream Recipients.</h4>
|
||||
|
||||
<p>Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.</p>
|
||||
|
||||
<p>An “entity transaction” is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party\'s predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.</p>
|
||||
|
||||
<p>You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.</p>
|
||||
|
||||
<h4><a name="section11"></a>11. Patents.</h4>
|
||||
|
||||
<p>A “contributor” is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor\'s “contributor version”.</p>
|
||||
|
||||
<p>A contributor\'s “essential patent claims” are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, “control” includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.</p>
|
||||
|
||||
<p>Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor\'s essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.</p>
|
||||
|
||||
<p>In the following three paragraphs, a “patent license” is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To “grant” such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.</p>
|
||||
|
||||
<p>If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. “Knowingly relying” means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient\'s use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.</p>
|
||||
|
||||
<p>If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.</p>
|
||||
|
||||
<p>A patent license is “discriminatory” if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.</p>
|
||||
|
||||
<p>Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.</p>
|
||||
|
||||
<h4><a name="section12"></a>12. No Surrender of Others\' Freedom.</h4>
|
||||
|
||||
<p>If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.</p>
|
||||
|
||||
<h4><a name="section13"></a>13. Use with the GNU Affero General Public License.</h4>
|
||||
|
||||
<p>Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.</p>
|
||||
|
||||
<h4><a name="section14"></a>14. Revised Versions of this License.</h4>
|
||||
|
||||
<p>The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.</p>
|
||||
|
||||
<p>Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License “or any later version” applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.</p>
|
||||
|
||||
<p>If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy\'s
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.</p>
|
||||
|
||||
<p>Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.</p>
|
||||
|
||||
<h4><a name="section15"></a>15. Disclaimer of Warranty.</h4>
|
||||
|
||||
<p>THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.</p>
|
||||
|
||||
<h4><a name="section16"></a>16. Limitation of Liability.</h4>
|
||||
|
||||
<p>IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.</p>
|
||||
|
||||
<h4><a name="section17"></a>17. Interpretation of Sections 15 and 16.</h4>
|
||||
|
||||
<p>If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.</p>
|
||||
|
||||
<p>END OF TERMS AND CONDITIONS</p>
|
||||
@@ -1 +0,0 @@
|
||||
{"ar_SA":["abdunnasir"],"bg":["dpa_transifex"],"ca":["Kintu","jordibrus","zagur"],"cs":["pavelb","svetlemodry","tomas.odehnal"],"da":["Tntdruid_","knutztar","mjjzf","twikedk"],"de":["Atalanttore","TheName","Wyrrrd","YvanM","amandablue","anestiskaci","corppneq","crit12","hammaschlach","maxkl","nicolas_git","owncube"],"el":["KristinaQejvanaj","anestiskaci","diamond_gr"],"es":["Ark74","Elhea","GranPC","aluaces","jcvielma","plaguna","polkhas","xphnx"],"eu":["Osoitz","Thadah","cockeredradiation"],"fa":["Numb","ahangarha","amiraliakbari","joojoojoo","maryambehzi","mtashackori","taranehsaei"],"fr":["AlainR","Amadeen","Floflr","JorisBodin","Llorc","LoiX07","Novick","Poussinou","Thecross","YvanM","alkino2","boutil","callmemagnus","chrcha","grenatrad","jokx","mathieugfortin","paullbn","vincen","ÉricB."],"fr_FR":["Llorc","Poussinou","chrcha"],"gl":["aluaces","pikamoku"],"hu":["Roshek","infeeeee","jtg"],"it":["Damtux","FranzMari","ed0","malaerba","noccio","nwandy","rickyroo","technezio"],"it_IT":["malaerba"],"ja":["Naofumi","yanorei32"],"nb_NO":["elonus"],"nl":["XtremeNova","davtemp","dehart","erikhubers","frankyboy1963"],"pl":["TORminator","TheName","Valdnet","gsz","mg6","oskarjakiela"],"pt":["amalvarenga","wanderlei.huttel"],"pt_BR":["wanderlei.huttel"],"ru":["aigoshin","anm","ashed","astalavister","nick.savin","vaddd"],"sk_SK":["brango67","tiborepcek"],"sl_SI":["MrLaaky","uroszor"],"sr":["daimonion"],"sv":["Mikaelb","campbelldavid"],"szl":["chlodny"],"tr_TR":["ooguz","pultars"],"uk":["androsua","olexn","twixi007"],"uk_UA":["astalavister"],"zh_CN":["anolir","jxj2zzz79pfp9bpo","linuxbckp","mofitt2016","oksjd","phy","spice2wolf"],"zh_TW":["linuxbckp","mofitt2016","phy","waiabsfabuloushk"]}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import android.app.Application
|
||||
import android.os.StrictMode
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.work.Configuration
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.syncadapter.AccountsUpdatedListener
|
||||
import at.bitfire.davdroid.ui.DebugInfoActivity
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import at.bitfire.davdroid.ui.UiUtils
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import java.util.logging.Level
|
||||
import javax.inject.Inject
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
@HiltAndroidApp
|
||||
class App: Application(), Thread.UncaughtExceptionHandler, Configuration.Provider {
|
||||
|
||||
@Inject lateinit var accountsUpdatedListener: AccountsUpdatedListener
|
||||
@Inject lateinit var workerFactory: HiltWorkerFactory
|
||||
|
||||
override val workManagerConfiguration: Configuration
|
||||
get() = Configuration.Builder()
|
||||
.setWorkerFactory(workerFactory)
|
||||
.build()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Logger.initialize(this)
|
||||
|
||||
if (BuildConfig.DEBUG)
|
||||
// debug builds
|
||||
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
|
||||
.detectActivityLeaks()
|
||||
.detectFileUriExposure()
|
||||
.detectLeakedClosableObjects()
|
||||
.detectLeakedRegistrationObjects()
|
||||
.detectLeakedSqlLiteObjects()
|
||||
.penaltyLog()
|
||||
.build())
|
||||
else // if (BuildConfig.FLAVOR == FLAVOR_STANDARD)
|
||||
// handle uncaught exceptions in non-debug standard flavor
|
||||
Thread.setDefaultUncaughtExceptionHandler(this)
|
||||
|
||||
NotificationUtils.createChannels(this)
|
||||
|
||||
// set light/dark mode
|
||||
UiUtils.updateTheme(this) // when this is called in the asynchronous thread below, it recreates
|
||||
// some current activity and causes an IllegalStateException in rare cases
|
||||
|
||||
// don't block UI for some background checks
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
thread {
|
||||
// watch for account changes/deletions
|
||||
accountsUpdatedListener.listen()
|
||||
|
||||
// watch installed/removed tasks apps over whole app lifetime and update sync settings accordingly
|
||||
TasksAppWatcher.watchInstalledTaskApps(this, GlobalScope)
|
||||
|
||||
// create/update app shortcuts
|
||||
UiUtils.updateShortcuts(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun uncaughtException(t: Thread, e: Throwable) {
|
||||
Logger.log.log(Level.SEVERE, "Unhandled exception!", e)
|
||||
|
||||
val intent = DebugInfoActivity.IntentBuilder(this)
|
||||
.withCause(e)
|
||||
.newTask()
|
||||
.build()
|
||||
startActivity(intent)
|
||||
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
|
||||
/**
|
||||
* Brand-specific constants like (non-theme) colors, homepage URLs etc.
|
||||
*/
|
||||
object Constants {
|
||||
|
||||
const val DAVDROID_GREEN_RGBA = 0xFF8bc34a.toInt()
|
||||
|
||||
val HOMEPAGE_URL = "https://www.davx5.com".toUri()
|
||||
const val HOMEPAGE_PATH_FAQ = "faq"
|
||||
const val HOMEPAGE_PATH_FAQ_SYNC_NOT_RUN = "synchronization-is-not-run-as-expected"
|
||||
const val HOMEPAGE_PATH_FAQ_LOCATION_PERMISSION = "wifi-ssid-restriction-location-permission"
|
||||
const val HOMEPAGE_PATH_OPEN_SOURCE = "donate"
|
||||
const val HOMEPAGE_PATH_PRIVACY = "privacy"
|
||||
const val HOMEPAGE_PATH_TESTED_SERVICES = "tested-with"
|
||||
|
||||
val MANUAL_URL = "https://manual.davx5.com".toUri()
|
||||
const val MANUAL_PATH_ACCOUNTS_COLLECTIONS = "accounts_collections.html"
|
||||
const val MANUAL_FRAGMENT_SERVICE_DISCOVERY = "how-does-service-discovery-work"
|
||||
const val MANUAL_PATH_WEBDAV_MOUNTS = "webdav_mounts.html"
|
||||
|
||||
val COMMUNITY_URL = "https://github.com/bitfireAT/davx5-ose/discussions".toUri()
|
||||
|
||||
val FEDIVERSE_HANDLE = "@davx5app@fosstodon.org"
|
||||
val FEDIVERSE_URL = "https://fosstodon.org/@davx5app".toUri()
|
||||
|
||||
/**
|
||||
* Appends query parameters for anonymized usage statistics (app ID, version).
|
||||
* Can be used by the called Website to get an idea of which versions etc. are currently used.
|
||||
*
|
||||
* @param context optional info about from where the URL was opened (like a specific Activity)
|
||||
*/
|
||||
fun Uri.Builder.withStatParams(context: String? = null): Uri.Builder {
|
||||
appendQueryParameter("pk_campaign", BuildConfig.APPLICATION_ID)
|
||||
appendQueryParameter("app-version", BuildConfig.VERSION_NAME)
|
||||
|
||||
if (context != null)
|
||||
appendQueryParameter("pk_kwd", context)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import android.accounts.Account
|
||||
|
||||
class InvalidAccountException(account: Account): Exception("Invalid account: $account")
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import android.content.Context
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.util.TaskUtils
|
||||
import at.bitfire.davdroid.util.packageChangedFlow
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Watches whether a tasks app has been installed or uninstalled and updates
|
||||
* the selected tasks app and task sync settings accordingly.
|
||||
*/
|
||||
object TasksAppWatcher {
|
||||
|
||||
fun watchInstalledTaskApps(context: Context, externalScope: CoroutineScope) {
|
||||
externalScope.launch(Dispatchers.Default) {
|
||||
packageChangedFlow(context).collect {
|
||||
onPackageChanged(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onPackageChanged(context: Context) {
|
||||
val currentProvider = TaskUtils.currentProvider(context)
|
||||
Logger.log.info("App launched or package (un)installed; current tasks provider = $currentProvider")
|
||||
|
||||
if (currentProvider == null) {
|
||||
// Iterate through all supported providers and select one, if available.
|
||||
var providerSelected = false
|
||||
for (provider in TaskProvider.ProviderName.entries) {
|
||||
val available = context.packageManager.resolveContentProvider(provider.authority, 0) != null
|
||||
if (available) {
|
||||
Logger.log.info("Selecting new tasks provider: $provider")
|
||||
TaskUtils.selectProvider(context, provider)
|
||||
providerSelected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!providerSelected)
|
||||
// no provider available (anymore), also clear setting and sync
|
||||
TaskUtils.selectProvider(context, null)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.util.Collections
|
||||
|
||||
class TextTable(
|
||||
vararg val headers: String
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun indent(str: String, pos: Int): String =
|
||||
" ".repeat(pos) +
|
||||
str.split('\n').joinToString("\n" + " ".repeat(pos))
|
||||
|
||||
}
|
||||
|
||||
|
||||
private val lines = mutableListOf<Array<String>>()
|
||||
|
||||
fun addLine(vararg values: Any?) {
|
||||
if (values.size != headers.size)
|
||||
throw IllegalArgumentException("Table line must have ${headers.size} column(s)")
|
||||
lines += values.map {
|
||||
it?.toString() ?: "—"
|
||||
}.toTypedArray()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val sb = StringBuilder()
|
||||
|
||||
val headerWidths = headers.map { it.length }
|
||||
val colWidths = Array<Int>(headers.size) { colIdx ->
|
||||
Collections.max(listOf(headerWidths[colIdx]) + lines.map { it[colIdx] }.map { it.length })
|
||||
}
|
||||
|
||||
// first line
|
||||
sb.append("\n┌")
|
||||
for (colIdx in headers.indices)
|
||||
sb .append(StringUtils.repeat('─', colWidths[colIdx] + 2))
|
||||
.append(if (colIdx == headers.size - 1) '┐' else '┬')
|
||||
sb.append('\n')
|
||||
|
||||
// header
|
||||
sb.append('│')
|
||||
for (colIdx in headers.indices)
|
||||
sb .append(' ')
|
||||
.append(StringUtils.rightPad(headers[colIdx], colWidths[colIdx] + 1))
|
||||
.append('│')
|
||||
sb.append('\n')
|
||||
|
||||
// separator between header and body
|
||||
sb.append('├')
|
||||
for (colIdx in headers.indices) {
|
||||
sb .append(StringUtils.repeat('─', colWidths[colIdx] + 2))
|
||||
.append(if (colIdx == headers.size - 1) '┤' else '┼')
|
||||
}
|
||||
sb.append('\n')
|
||||
|
||||
// body
|
||||
for (line in lines) {
|
||||
for (colIdx in headers.indices)
|
||||
sb .append("│ ")
|
||||
.append(StringUtils.rightPad(line[colIdx], colWidths[colIdx] + 1))
|
||||
sb.append("│\n")
|
||||
}
|
||||
|
||||
// last line
|
||||
sb.append("└")
|
||||
for (colIdx in headers.indices) {
|
||||
sb .append("─".repeat(colWidths[colIdx] + 2))
|
||||
.append(if (colIdx == headers.size - 1) '┘' else '┴')
|
||||
}
|
||||
sb.append("\n\n")
|
||||
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import android.accounts.AccountManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.sqlite.SQLiteQueryBuilder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.database.getStringOrNull
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.DeleteColumn
|
||||
import androidx.room.ProvidedAutoMigrationSpec
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.TextTable
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
|
||||
import at.bitfire.davdroid.ui.AccountsActivity
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.io.Writer
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Suppress("ClassName")
|
||||
@Database(entities = [
|
||||
Service::class,
|
||||
HomeSet::class,
|
||||
Collection::class,
|
||||
Principal::class,
|
||||
SyncStats::class,
|
||||
WebDavDocument::class,
|
||||
WebDavMount::class
|
||||
], exportSchema = true, version = 13, autoMigrations = [
|
||||
AutoMigration(from = 9, to = 10),
|
||||
AutoMigration(from = 10, to = 11),
|
||||
AutoMigration(from = 11, to = 12, spec = AppDatabase.AutoMigration11_12::class),
|
||||
AutoMigration(from = 12, to = 13)
|
||||
])
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase: RoomDatabase() {
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object AppDatabaseModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun appDatabase(@ApplicationContext context: Context): AppDatabase =
|
||||
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "services.db")
|
||||
.addMigrations(*migrations)
|
||||
.addAutoMigrationSpec(AutoMigration11_12(context))
|
||||
.fallbackToDestructiveMigration() // as a last fallback, recreate database instead of crashing
|
||||
.addCallback(object: Callback() {
|
||||
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
|
||||
val nm = NotificationManagerCompat.from(context)
|
||||
val launcherIntent = Intent(context, AccountsActivity::class.java)
|
||||
val notify = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_GENERAL)
|
||||
.setSmallIcon(R.drawable.ic_warning_notify)
|
||||
.setContentTitle(context.getString(R.string.database_destructive_migration_title))
|
||||
.setContentText(context.getString(R.string.database_destructive_migration_text))
|
||||
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||
.setContentIntent(PendingIntent.getActivity(context, 0, launcherIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
nm.notifyIfPossible(NotificationUtils.NOTIFY_DATABASE_CORRUPTED, notify)
|
||||
|
||||
// remove all accounts because they're unfortunately useless without database
|
||||
val am = AccountManager.get(context)
|
||||
for (account in am.getAccountsByType(context.getString(R.string.account_type)))
|
||||
am.removeAccount(account, null, null)
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
// auto migrations
|
||||
|
||||
@ProvidedAutoMigrationSpec
|
||||
@DeleteColumn(tableName = "collection", columnName = "owner")
|
||||
class AutoMigration11_12(val context: Context): AutoMigrationSpec {
|
||||
override fun onPostMigrate(db: SupportSQLiteDatabase) {
|
||||
Logger.log.info("Database update to v12, refreshing services to get display names of owners")
|
||||
db.query("SELECT id FROM service", arrayOf()).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val serviceId = cursor.getLong(0)
|
||||
RefreshCollectionsWorker.enqueue(context, serviceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
// manual migrations
|
||||
|
||||
val migrations: Array<Migration> = arrayOf(
|
||||
object : Migration(8, 9) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("CREATE TABLE syncstats (" +
|
||||
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
|
||||
"collectionId INTEGER NOT NULL REFERENCES collection(id) ON DELETE CASCADE," +
|
||||
"authority TEXT NOT NULL," +
|
||||
"lastSync INTEGER NOT NULL)")
|
||||
db.execSQL("CREATE UNIQUE INDEX index_syncstats_collectionId_authority ON syncstats(collectionId, authority)")
|
||||
|
||||
db.execSQL("CREATE INDEX index_collection_url ON collection(url)")
|
||||
}
|
||||
},
|
||||
|
||||
object : Migration(7, 8) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE homeset ADD COLUMN personal INTEGER NOT NULL DEFAULT 1")
|
||||
db.execSQL("ALTER TABLE collection ADD COLUMN homeSetId INTEGER DEFAULT NULL REFERENCES homeset(id) ON DELETE SET NULL")
|
||||
db.execSQL("ALTER TABLE collection ADD COLUMN owner TEXT DEFAULT NULL")
|
||||
db.execSQL("CREATE INDEX index_collection_homeSetId_type ON collection(homeSetId, type)")
|
||||
}
|
||||
},
|
||||
|
||||
object : Migration(6, 7) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE homeset ADD COLUMN privBind INTEGER NOT NULL DEFAULT 1")
|
||||
db.execSQL("ALTER TABLE homeset ADD COLUMN displayName TEXT DEFAULT NULL")
|
||||
}
|
||||
},
|
||||
|
||||
object : Migration(5, 6) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
val sql = arrayOf(
|
||||
// migrate "services" to "service": rename columns, make id NOT NULL
|
||||
"CREATE TABLE service(" +
|
||||
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
|
||||
"accountName TEXT NOT NULL," +
|
||||
"type TEXT NOT NULL," +
|
||||
"principal TEXT DEFAULT NULL" +
|
||||
")",
|
||||
"CREATE UNIQUE INDEX index_service_accountName_type ON service(accountName, type)",
|
||||
"INSERT INTO service(id, accountName, type, principal) SELECT _id, accountName, service, principal FROM services",
|
||||
"DROP TABLE services",
|
||||
|
||||
// migrate "homesets" to "homeset": rename columns, make id NOT NULL
|
||||
"CREATE TABLE homeset(" +
|
||||
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
|
||||
"serviceId INTEGER NOT NULL," +
|
||||
"url TEXT NOT NULL," +
|
||||
"FOREIGN KEY (serviceId) REFERENCES service(id) ON DELETE CASCADE" +
|
||||
")",
|
||||
"CREATE UNIQUE INDEX index_homeset_serviceId_url ON homeset(serviceId, url)",
|
||||
"INSERT INTO homeset(id, serviceId, url) SELECT _id, serviceID, url FROM homesets",
|
||||
"DROP TABLE homesets",
|
||||
|
||||
// migrate "collections" to "collection": rename columns, make id NOT NULL
|
||||
"CREATE TABLE collection(" +
|
||||
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT," +
|
||||
"serviceId INTEGER NOT NULL," +
|
||||
"type TEXT NOT NULL," +
|
||||
"url TEXT NOT NULL," +
|
||||
"privWriteContent INTEGER NOT NULL DEFAULT 1," +
|
||||
"privUnbind INTEGER NOT NULL DEFAULT 1," +
|
||||
"forceReadOnly INTEGER NOT NULL DEFAULT 0," +
|
||||
"displayName TEXT DEFAULT NULL," +
|
||||
"description TEXT DEFAULT NULL," +
|
||||
"color INTEGER DEFAULT NULL," +
|
||||
"timezone TEXT DEFAULT NULL," +
|
||||
"supportsVEVENT INTEGER DEFAULT NULL," +
|
||||
"supportsVTODO INTEGER DEFAULT NULL," +
|
||||
"supportsVJOURNAL INTEGER DEFAULT NULL," +
|
||||
"source TEXT DEFAULT NULL," +
|
||||
"sync INTEGER NOT NULL DEFAULT 0," +
|
||||
"FOREIGN KEY (serviceId) REFERENCES service(id) ON DELETE CASCADE" +
|
||||
")",
|
||||
"CREATE INDEX index_collection_serviceId_type ON collection(serviceId,type)",
|
||||
"INSERT INTO collection(id, serviceId, type, url, privWriteContent, privUnbind, forceReadOnly, displayName, description, color, timezone, supportsVEVENT, supportsVTODO, source, sync) " +
|
||||
"SELECT _id, serviceID, type, url, privWriteContent, privUnbind, forceReadOnly, displayName, description, color, timezone, supportsVEVENT, supportsVTODO, source, sync FROM collections",
|
||||
"DROP TABLE collections"
|
||||
)
|
||||
sql.forEach { db.execSQL(it) }
|
||||
}
|
||||
},
|
||||
|
||||
object : Migration(4, 5) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE collections ADD COLUMN privWriteContent INTEGER DEFAULT 0 NOT NULL")
|
||||
db.execSQL("UPDATE collections SET privWriteContent=NOT readOnly")
|
||||
|
||||
db.execSQL("ALTER TABLE collections ADD COLUMN privUnbind INTEGER DEFAULT 0 NOT NULL")
|
||||
db.execSQL("UPDATE collections SET privUnbind=NOT readOnly")
|
||||
|
||||
// there's no DROP COLUMN in SQLite, so just keep the "readOnly" column
|
||||
}
|
||||
},
|
||||
|
||||
object : Migration(3, 4) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE collections ADD COLUMN forceReadOnly INTEGER DEFAULT 0 NOT NULL")
|
||||
}
|
||||
},
|
||||
|
||||
object : Migration(2, 3) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
// We don't have access to the context in a Room migration now, so
|
||||
// we will just drop those settings from old DAVx5 versions.
|
||||
Logger.log.warning("Dropping settings distrustSystemCerts and overrideProxy*")
|
||||
|
||||
/*val edit = PreferenceManager.getDefaultSharedPreferences(context).edit()
|
||||
try {
|
||||
db.query("settings", arrayOf("setting", "value"), null, null, null, null, null).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
when (cursor.getString(0)) {
|
||||
"distrustSystemCerts" -> edit.putBoolean(App.DISTRUST_SYSTEM_CERTIFICATES, cursor.getInt(1) != 0)
|
||||
"overrideProxy" -> edit.putBoolean(App.OVERRIDE_PROXY, cursor.getInt(1) != 0)
|
||||
"overrideProxyHost" -> edit.putString(App.OVERRIDE_PROXY_HOST, cursor.getString(1))
|
||||
"overrideProxyPort" -> edit.putInt(App.OVERRIDE_PROXY_PORT, cursor.getInt(1))
|
||||
|
||||
StartupDialogFragment.HINT_GOOGLE_PLAY_ACCOUNTS_REMOVED ->
|
||||
edit.putBoolean(StartupDialogFragment.HINT_GOOGLE_PLAY_ACCOUNTS_REMOVED, cursor.getInt(1) != 0)
|
||||
StartupDialogFragment.HINT_OPENTASKS_NOT_INSTALLED ->
|
||||
edit.putBoolean(StartupDialogFragment.HINT_OPENTASKS_NOT_INSTALLED, cursor.getInt(1) != 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
db.execSQL("DROP TABLE settings")
|
||||
} finally {
|
||||
edit.apply()
|
||||
}*/
|
||||
}
|
||||
},
|
||||
|
||||
object : Migration(1, 2) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE collections ADD COLUMN type TEXT NOT NULL DEFAULT ''")
|
||||
db.execSQL("ALTER TABLE collections ADD COLUMN source TEXT DEFAULT NULL")
|
||||
db.execSQL("UPDATE collections SET type=(" +
|
||||
"SELECT CASE service WHEN ? THEN ? ELSE ? END " +
|
||||
"FROM services WHERE _id=collections.serviceID" +
|
||||
")",
|
||||
arrayOf("caldav", "CALENDAR", "ADDRESS_BOOK"))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
// DAOs
|
||||
|
||||
abstract fun serviceDao(): ServiceDao
|
||||
abstract fun homeSetDao(): HomeSetDao
|
||||
abstract fun collectionDao(): CollectionDao
|
||||
abstract fun principalDao(): PrincipalDao
|
||||
abstract fun syncStatsDao(): SyncStatsDao
|
||||
abstract fun webDavDocumentDao(): WebDavDocumentDao
|
||||
abstract fun webDavMountDao(): WebDavMountDao
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
fun dump(writer: Writer, ignoreTables: Array<String>) {
|
||||
val db = openHelper.readableDatabase
|
||||
db.beginTransactionNonExclusive()
|
||||
|
||||
// iterate through all tables
|
||||
db.query(SQLiteQueryBuilder.buildQueryString(false, "sqlite_master", arrayOf("name"), "type='table'", null, null, null, null)).use { cursorTables ->
|
||||
while (cursorTables.moveToNext()) {
|
||||
val tableName = cursorTables.getString(0)
|
||||
if (ignoreTables.contains(tableName)) {
|
||||
writer.append("$tableName: ")
|
||||
db.query("SELECT COUNT(*) FROM $tableName").use { cursor ->
|
||||
if (cursor.moveToNext())
|
||||
writer.append("${cursor.getInt(0)} row(s), data not listed here\n\n")
|
||||
}
|
||||
} else {
|
||||
writer.append("$tableName\n")
|
||||
db.query("SELECT * FROM $tableName").use { cursor ->
|
||||
val table = TextTable(*cursor.columnNames)
|
||||
val cols = cursor.columnCount
|
||||
// print rows
|
||||
while (cursor.moveToNext()) {
|
||||
val values = Array(cols) { idx -> cursor.getStringOrNull(idx) }
|
||||
table.addLine(*values)
|
||||
}
|
||||
writer.append(table.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import at.bitfire.dav4jvm.Response
|
||||
import at.bitfire.dav4jvm.UrlUtils
|
||||
import at.bitfire.dav4jvm.property.caldav.CalendarColor
|
||||
import at.bitfire.dav4jvm.property.caldav.CalendarDescription
|
||||
import at.bitfire.dav4jvm.property.caldav.CalendarTimezone
|
||||
import at.bitfire.dav4jvm.property.caldav.Source
|
||||
import at.bitfire.dav4jvm.property.caldav.SupportedCalendarComponentSet
|
||||
import at.bitfire.dav4jvm.property.carddav.AddressbookDescription
|
||||
import at.bitfire.dav4jvm.property.push.PushTransports
|
||||
import at.bitfire.dav4jvm.property.push.Topic
|
||||
import at.bitfire.dav4jvm.property.webdav.CurrentUserPrivilegeSet
|
||||
import at.bitfire.dav4jvm.property.webdav.DisplayName
|
||||
import at.bitfire.dav4jvm.property.webdav.ResourceType
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
||||
@Entity(tableName = "collection",
|
||||
foreignKeys = [
|
||||
ForeignKey(entity = Service::class, parentColumns = arrayOf("id"), childColumns = arrayOf("serviceId"), onDelete = ForeignKey.CASCADE),
|
||||
ForeignKey(entity = HomeSet::class, parentColumns = arrayOf("id"), childColumns = arrayOf("homeSetId"), onDelete = ForeignKey.SET_NULL),
|
||||
ForeignKey(entity = Principal::class, parentColumns = arrayOf("id"), childColumns = arrayOf("ownerId"), onDelete = ForeignKey.SET_NULL)
|
||||
],
|
||||
indices = [
|
||||
Index("serviceId","type"),
|
||||
Index("homeSetId","type"),
|
||||
Index("url")
|
||||
]
|
||||
)
|
||||
data class Collection(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0,
|
||||
|
||||
/**
|
||||
* Service, which this collection belongs to. Services are unique, so a [Collection] is uniquely
|
||||
* identifiable via its [serviceId] and [url].
|
||||
*/
|
||||
var serviceId: Long = 0,
|
||||
|
||||
/**
|
||||
* A home set this collection belongs to. Multiple homesets are not supported.
|
||||
* If *null* the collection is considered homeless.
|
||||
*/
|
||||
var homeSetId: Long? = null,
|
||||
|
||||
/**
|
||||
* Principal who is owner of this collection.
|
||||
*/
|
||||
var ownerId: Long? = null,
|
||||
|
||||
/**
|
||||
* Type of service. CalDAV or CardDAV
|
||||
*/
|
||||
var type: String,
|
||||
|
||||
/**
|
||||
* Address where this collection lives - with trailing slash
|
||||
*/
|
||||
var url: HttpUrl,
|
||||
|
||||
var privWriteContent: Boolean = true,
|
||||
var privUnbind: Boolean = true,
|
||||
var forceReadOnly: Boolean = false,
|
||||
|
||||
var displayName: String? = null,
|
||||
var description: String? = null,
|
||||
|
||||
// CalDAV only
|
||||
var color: Int? = null,
|
||||
|
||||
/** timezone definition (full VTIMEZONE) - not a TZID! **/
|
||||
var timezone: String? = null,
|
||||
|
||||
/** whether the collection supports VEVENT; in case of calendars: null means true */
|
||||
var supportsVEVENT: Boolean? = null,
|
||||
|
||||
/** whether the collection supports VTODO; in case of calendars: null means true */
|
||||
var supportsVTODO: Boolean? = null,
|
||||
|
||||
/** whether the collection supports VJOURNAL; in case of calendars: null means true */
|
||||
var supportsVJOURNAL: Boolean? = null,
|
||||
|
||||
/** Webcal subscription source URL */
|
||||
var source: HttpUrl? = null,
|
||||
|
||||
/** whether this collection has been selected for synchronization */
|
||||
var sync: Boolean = false,
|
||||
|
||||
/** WebDAV-Push topic */
|
||||
var pushTopic: String? = null,
|
||||
|
||||
/** WebDAV-Push: whether this collection supports the Web Push Transport */
|
||||
@ColumnInfo(defaultValue = "0")
|
||||
var supportsWebPush: Boolean = false,
|
||||
|
||||
/** WebDAV-Push subscription URL */
|
||||
var pushSubscription: String? = null,
|
||||
|
||||
/** when the [pushSubscription] was created/updated (used to determine whether we need to re-subscribe) */
|
||||
var pushSubscriptionCreated: Long? = null
|
||||
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
const val TYPE_ADDRESSBOOK = "ADDRESS_BOOK"
|
||||
const val TYPE_CALENDAR = "CALENDAR"
|
||||
const val TYPE_WEBCAL = "WEBCAL"
|
||||
|
||||
/**
|
||||
* Generates a collection entity from a WebDAV response.
|
||||
* @param dav WebDAV response
|
||||
* @return null if the response doesn't represent a collection
|
||||
*/
|
||||
fun fromDavResponse(dav: Response): Collection? {
|
||||
val url = UrlUtils.withTrailingSlash(dav.href)
|
||||
val type: String = dav[ResourceType::class.java]?.let { resourceType ->
|
||||
when {
|
||||
resourceType.types.contains(ResourceType.ADDRESSBOOK) -> TYPE_ADDRESSBOOK
|
||||
resourceType.types.contains(ResourceType.CALENDAR) -> TYPE_CALENDAR
|
||||
resourceType.types.contains(ResourceType.SUBSCRIBED) -> TYPE_WEBCAL
|
||||
else -> null
|
||||
}
|
||||
} ?: return null
|
||||
|
||||
var privWriteContent = true
|
||||
var privUnbind = true
|
||||
dav[CurrentUserPrivilegeSet::class.java]?.let { privilegeSet ->
|
||||
privWriteContent = privilegeSet.mayWriteContent
|
||||
privUnbind = privilegeSet.mayUnbind
|
||||
}
|
||||
|
||||
val displayName = StringUtils.trimToNull(dav[DisplayName::class.java]?.displayName)
|
||||
|
||||
var description: String? = null
|
||||
var color: Int? = null
|
||||
var timezone: String? = null
|
||||
var supportsVEVENT: Boolean? = null
|
||||
var supportsVTODO: Boolean? = null
|
||||
var supportsVJOURNAL: Boolean? = null
|
||||
var source: HttpUrl? = null
|
||||
when (type) {
|
||||
TYPE_ADDRESSBOOK -> {
|
||||
dav[AddressbookDescription::class.java]?.let { description = it.description }
|
||||
}
|
||||
TYPE_CALENDAR, TYPE_WEBCAL -> {
|
||||
dav[CalendarDescription::class.java]?.let { description = it.description }
|
||||
dav[CalendarColor::class.java]?.let { color = it.color }
|
||||
dav[CalendarTimezone::class.java]?.let { timezone = it.vTimeZone }
|
||||
|
||||
if (type == TYPE_CALENDAR) {
|
||||
supportsVEVENT = true
|
||||
supportsVTODO = true
|
||||
supportsVJOURNAL = true
|
||||
dav[SupportedCalendarComponentSet::class.java]?.let {
|
||||
supportsVEVENT = it.supportsEvents
|
||||
supportsVTODO = it.supportsTasks
|
||||
supportsVJOURNAL = it.supportsJournal
|
||||
}
|
||||
} else { // Type.WEBCAL
|
||||
dav[Source::class.java]?.let {
|
||||
source = it.hrefs.firstOrNull()?.let { rawHref ->
|
||||
val href = rawHref
|
||||
.replace("^webcal://".toRegex(), "http://")
|
||||
.replace("^webcals://".toRegex(), "https://")
|
||||
href.toHttpUrlOrNull()
|
||||
}
|
||||
}
|
||||
supportsVEVENT = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WebDAV-Push
|
||||
var supportsWebPush = false
|
||||
dav[PushTransports::class.java]?.let { pushTransports ->
|
||||
supportsWebPush = pushTransports.hasWebPush()
|
||||
}
|
||||
val pushTopic = dav[Topic::class.java]?.topic
|
||||
|
||||
return Collection(
|
||||
type = type,
|
||||
url = url,
|
||||
privWriteContent = privWriteContent,
|
||||
privUnbind = privUnbind,
|
||||
displayName = displayName,
|
||||
description = description,
|
||||
color = color,
|
||||
timezone = timezone,
|
||||
supportsVEVENT = supportsVEVENT,
|
||||
supportsVTODO = supportsVTODO,
|
||||
supportsVJOURNAL = supportsVJOURNAL,
|
||||
source = source,
|
||||
supportsWebPush = supportsWebPush,
|
||||
pushTopic = pushTopic
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// calculated properties
|
||||
fun title() = displayName ?: DavUtils.lastSegmentOfUrl(url)
|
||||
fun readOnly() = forceReadOnly || !privWriteContent
|
||||
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
|
||||
@Dao
|
||||
interface CollectionDao {
|
||||
|
||||
@Query("SELECT DISTINCT color FROM collection WHERE serviceId=:id")
|
||||
fun colorsByServiceLive(id: Long): LiveData<List<Int>>
|
||||
|
||||
@Query("SELECT * FROM collection WHERE id=:id")
|
||||
fun get(id: Long): Collection?
|
||||
|
||||
@Query("SELECT * FROM collection WHERE id=:id")
|
||||
fun getLive(id: Long): LiveData<Collection>
|
||||
|
||||
@Query("SELECT * FROM collection WHERE serviceId=:serviceId")
|
||||
fun getByService(serviceId: Long): List<Collection>
|
||||
|
||||
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND homeSetId IS :homeSetId")
|
||||
fun getByServiceAndHomeset(serviceId: Long, homeSetId: Long?): List<Collection>
|
||||
|
||||
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type=:type ORDER BY displayName COLLATE NOCASE, url COLLATE NOCASE")
|
||||
fun getByServiceAndType(serviceId: Long, type: String): List<Collection>
|
||||
|
||||
/**
|
||||
* Returns collections which
|
||||
* - support VEVENT and/or VTODO (= supported calendar collections), or
|
||||
* - have supportsVEVENT = supportsVTODO = null (= address books)
|
||||
*/
|
||||
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type=:type " +
|
||||
"AND (supportsVTODO OR supportsVEVENT OR supportsVJOURNAL OR (supportsVEVENT IS NULL AND supportsVTODO IS NULL AND supportsVJOURNAL IS NULL)) ORDER BY displayName COLLATE NOCASE, URL COLLATE NOCASE")
|
||||
fun pageByServiceAndType(serviceId: Long, type: String): PagingSource<Int, Collection>
|
||||
|
||||
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND sync")
|
||||
fun getByServiceAndSync(serviceId: Long): List<Collection>
|
||||
|
||||
@Query("SELECT collection.* FROM collection, homeset WHERE collection.serviceId=:serviceId AND type=:type AND homeSetId=homeset.id AND homeset.personal ORDER BY collection.displayName COLLATE NOCASE, collection.url COLLATE NOCASE")
|
||||
fun pagePersonalByServiceAndType(serviceId: Long, type: String): PagingSource<Int, Collection>
|
||||
|
||||
@Deprecated("Use getByServiceAndUrl instead")
|
||||
@Query("SELECT * FROM collection WHERE url=:url")
|
||||
fun getByUrl(url: String): Collection?
|
||||
|
||||
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND url=:url")
|
||||
fun getByServiceAndUrl(serviceId: Long, url: String): Collection?
|
||||
|
||||
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type='${Collection.TYPE_CALENDAR}' AND supportsVEVENT AND sync ORDER BY displayName COLLATE NOCASE, url COLLATE NOCASE")
|
||||
fun getSyncCalendars(serviceId: Long): List<Collection>
|
||||
|
||||
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type='${Collection.TYPE_CALENDAR}' AND (supportsVTODO OR supportsVJOURNAL) AND sync ORDER BY displayName COLLATE NOCASE, url COLLATE NOCASE")
|
||||
fun getSyncJtxCollections(serviceId: Long): List<Collection>
|
||||
|
||||
@Query("SELECT * FROM collection WHERE serviceId=:serviceId AND type='${Collection.TYPE_CALENDAR}' AND supportsVTODO AND sync ORDER BY displayName COLLATE NOCASE, url COLLATE NOCASE")
|
||||
fun getSyncTaskLists(serviceId: Long): List<Collection>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
fun insert(collection: Collection): Long
|
||||
|
||||
@Update
|
||||
fun update(collection: Collection)
|
||||
|
||||
@Query("UPDATE collection SET forceReadOnly=:forceReadOnly WHERE id=:id")
|
||||
fun updateForceReadOnly(id: Long, forceReadOnly: Boolean)
|
||||
|
||||
@Query("UPDATE collection SET sync=:sync WHERE id=:id")
|
||||
fun updateSync(id: Long, sync: Boolean)
|
||||
|
||||
/**
|
||||
* Tries to insert new row, but updates existing row if already present.
|
||||
* This method preserves the primary key, as opposed to using "@Insert(onConflict = OnConflictStrategy.REPLACE)"
|
||||
* which will create a new row with incremented ID and thus breaks entity relationships!
|
||||
*
|
||||
* @param collection Collection to be inserted or updated
|
||||
* @return ID of the row, that has been inserted or updated. -1 If the insert fails due to other reasons.
|
||||
*/
|
||||
@Transaction
|
||||
fun insertOrUpdateByUrl(collection: Collection): Long = getByServiceAndUrl(
|
||||
collection.serviceId,
|
||||
collection.url.toString()
|
||||
)?.let { localCollection ->
|
||||
update(collection.copy(id = localCollection.id))
|
||||
localCollection.id
|
||||
} ?: insert(collection)
|
||||
|
||||
/**
|
||||
* Inserts or updates the collection. On update it will not update flag values ([Collection.sync],
|
||||
* [Collection.forceReadOnly]), but use the values of the already existing collection.
|
||||
*
|
||||
* @param newCollection Collection to be inserted or updated
|
||||
*/
|
||||
fun insertOrUpdateByUrlAndRememberFlags(newCollection: Collection) {
|
||||
// remember locally set flags
|
||||
getByServiceAndUrl(newCollection.serviceId, newCollection.url.toString())?.let { oldCollection ->
|
||||
newCollection.sync = oldCollection.sync
|
||||
newCollection.forceReadOnly = oldCollection.forceReadOnly
|
||||
}
|
||||
|
||||
// commit to database
|
||||
insertOrUpdateByUrl(newCollection)
|
||||
}
|
||||
|
||||
@Delete
|
||||
fun delete(collection: Collection)
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
|
||||
class Converters {
|
||||
|
||||
@TypeConverter
|
||||
fun httpUrlToString(url: HttpUrl?) =
|
||||
url?.toString()
|
||||
|
||||
@TypeConverter
|
||||
fun mediaTypeToString(mediaType: MediaType?) =
|
||||
mediaType?.toString()
|
||||
|
||||
@TypeConverter
|
||||
fun stringToHttpUrl(url: String?): HttpUrl? =
|
||||
url?.toHttpUrlOrNull()
|
||||
|
||||
@TypeConverter
|
||||
fun stringToMediaType(mimeType: String?): MediaType? =
|
||||
mimeType?.toMediaTypeOrNull()
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import net.openid.appauth.AuthState
|
||||
|
||||
data class Credentials(
|
||||
val username: String? = null,
|
||||
val password: String? = null,
|
||||
|
||||
val certificateAlias: String? = null,
|
||||
|
||||
val authState: AuthState? = null
|
||||
) {
|
||||
|
||||
override fun toString(): String {
|
||||
val s = mutableListOf<String>()
|
||||
|
||||
if (username != null)
|
||||
s += "userName=$username"
|
||||
if (password != null)
|
||||
s += "password=*****"
|
||||
|
||||
if (certificateAlias != null)
|
||||
s += "certificateAlias=$certificateAlias"
|
||||
|
||||
if (authState != null)
|
||||
s += "authState=${authState.jsonSerializeString()}"
|
||||
|
||||
return "Credentials(" + s.joinToString(", ") + ")"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
@Entity(tableName = "homeset",
|
||||
foreignKeys = [
|
||||
ForeignKey(entity = Service::class, parentColumns = ["id"], childColumns = ["serviceId"], onDelete = ForeignKey.CASCADE)
|
||||
],
|
||||
indices = [
|
||||
// index by service; no duplicate URLs per service
|
||||
Index("serviceId", "url", unique = true)
|
||||
]
|
||||
)
|
||||
data class HomeSet(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long,
|
||||
|
||||
var serviceId: Long,
|
||||
|
||||
/**
|
||||
* Whether this homeset belongs to the [Service.principal] given by [serviceId].
|
||||
*/
|
||||
var personal: Boolean,
|
||||
|
||||
var url: HttpUrl,
|
||||
|
||||
var privBind: Boolean = true,
|
||||
|
||||
var displayName: String? = null
|
||||
)
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
|
||||
@Dao
|
||||
interface HomeSetDao {
|
||||
|
||||
@Query("SELECT * FROM homeset WHERE id=:homesetId")
|
||||
fun getById(homesetId: Long): HomeSet
|
||||
|
||||
@Query("SELECT * FROM homeset WHERE serviceId=:serviceId AND url=:url")
|
||||
fun getByUrl(serviceId: Long, url: String): HomeSet?
|
||||
|
||||
@Query("SELECT * FROM homeset WHERE serviceId=:serviceId")
|
||||
fun getByService(serviceId: Long): List<HomeSet>
|
||||
|
||||
@Query("SELECT * FROM homeset WHERE serviceId=:serviceId AND privBind")
|
||||
fun getBindableByService(serviceId: Long): List<HomeSet>
|
||||
|
||||
@Query("SELECT * FROM homeset WHERE serviceId=:serviceId AND privBind")
|
||||
fun getLiveBindableByService(serviceId: Long): LiveData<List<HomeSet>>
|
||||
|
||||
@Insert
|
||||
fun insert(homeSet: HomeSet): Long
|
||||
|
||||
@Update
|
||||
fun update(homeset: HomeSet)
|
||||
|
||||
/**
|
||||
* Tries to insert new row, but updates existing row if already present.
|
||||
* This method preserves the primary key, as opposed to using "@Insert(onConflict = OnConflictStrategy.REPLACE)"
|
||||
* which will create a new row with incremented ID and thus breaks entity relationships!
|
||||
*
|
||||
* @return ID of the row, that has been inserted or updated. -1 If the insert fails due to other reasons.
|
||||
*/
|
||||
@Transaction
|
||||
fun insertOrUpdateByUrl(homeset: HomeSet): Long =
|
||||
getByUrl(homeset.serviceId, homeset.url.toString())?.let { existingHomeset ->
|
||||
update(homeset.copy(id = existingHomeset.id))
|
||||
existingHomeset.id
|
||||
} ?: insert(homeset)
|
||||
|
||||
@Delete
|
||||
fun delete(homeset: HomeSet)
|
||||
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import at.bitfire.dav4jvm.Response
|
||||
import at.bitfire.dav4jvm.UrlUtils
|
||||
import at.bitfire.dav4jvm.property.webdav.DisplayName
|
||||
import at.bitfire.dav4jvm.property.webdav.ResourceType
|
||||
import okhttp3.HttpUrl
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
||||
@Entity(tableName = "principal",
|
||||
foreignKeys = [
|
||||
ForeignKey(entity = Service::class, parentColumns = arrayOf("id"), childColumns = arrayOf("serviceId"), onDelete = ForeignKey.CASCADE)
|
||||
],
|
||||
indices = [
|
||||
// index by service, urls are unique
|
||||
Index("serviceId", "url", unique = true)
|
||||
]
|
||||
)
|
||||
data class Principal(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0,
|
||||
var serviceId: Long,
|
||||
/** URL of the principal, always without trailing slash */
|
||||
var url: HttpUrl,
|
||||
var displayName: String? = null
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Generates a principal entity from a WebDAV response.
|
||||
* @param dav WebDAV response (make sure that you have queried `DAV:resource-type` and `DAV:display-name`)
|
||||
* @return generated principal data object (with `id`=0), `null` if the response doesn't represent a principal
|
||||
*/
|
||||
fun fromDavResponse(serviceId: Long, dav: Response): Principal? {
|
||||
// Check if response is a principal
|
||||
val resourceType = dav[ResourceType::class.java] ?: return null
|
||||
if (!resourceType.types.contains(ResourceType.PRINCIPAL))
|
||||
return null
|
||||
|
||||
// Try getting the display name of the principal
|
||||
val displayName: String? = StringUtils.trimToNull(
|
||||
dav[DisplayName::class.java]?.displayName
|
||||
)
|
||||
|
||||
// Create and return principal - even without it's display name
|
||||
return Principal(
|
||||
serviceId = serviceId,
|
||||
url = UrlUtils.omitTrailingSlash(dav.href),
|
||||
displayName = displayName
|
||||
)
|
||||
}
|
||||
|
||||
fun fromServiceAndUrl(service: Service, url: HttpUrl) = Principal(
|
||||
serviceId = service.id,
|
||||
url = UrlUtils.omitTrailingSlash(url)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
@Dao
|
||||
interface PrincipalDao {
|
||||
|
||||
@Query("SELECT * FROM principal WHERE id=:id")
|
||||
fun get(id: Long): Principal
|
||||
|
||||
@Query("SELECT * FROM principal WHERE id=:id")
|
||||
fun getLive(id: Long): LiveData<Principal?>
|
||||
|
||||
@Query("SELECT * FROM principal WHERE serviceId=:serviceId")
|
||||
fun getByService(serviceId: Long): List<Principal>
|
||||
|
||||
@Query("SELECT * FROM principal WHERE serviceId=:serviceId AND url=:url")
|
||||
fun getByUrl(serviceId: Long, url: HttpUrl): Principal?
|
||||
|
||||
/**
|
||||
* Gets all principals who do not own any collections
|
||||
*/
|
||||
@Query("SELECT * FROM principal WHERE principal.id NOT IN (SELECT ownerId FROM collection WHERE ownerId IS NOT NULL)")
|
||||
fun getAllWithoutCollections(): List<Principal>
|
||||
|
||||
@Insert
|
||||
fun insert(principal: Principal): Long
|
||||
|
||||
@Update
|
||||
fun update(principal: Principal)
|
||||
|
||||
@Delete
|
||||
fun delete(principal: Principal)
|
||||
|
||||
/**
|
||||
* Inserts, updates or just gets existing principal if its display name has not
|
||||
* changed (will not update/overwrite with null values).
|
||||
*
|
||||
* @param principal Principal to be inserted or updated
|
||||
* @return ID of the newly inserted or already existing principal
|
||||
*/
|
||||
fun insertOrUpdate(serviceId: Long, principal: Principal): Long =
|
||||
getByUrl(serviceId, principal.url)?.let { oldPrincipal ->
|
||||
if (principal.displayName != oldPrincipal.displayName)
|
||||
update(principal.copy(id = oldPrincipal.id))
|
||||
return oldPrincipal.id
|
||||
} ?: insert(principal)
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
/**
|
||||
* A service entity.
|
||||
*
|
||||
* Services represent accounts and are unique. They are of type CardDAV or CalDAV and may have an associated principal.
|
||||
*/
|
||||
@Entity(tableName = "service",
|
||||
indices = [
|
||||
// only one service per type and account
|
||||
Index("accountName", "type", unique = true)
|
||||
])
|
||||
data class Service(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long,
|
||||
|
||||
var accountName: String,
|
||||
var type: String,
|
||||
|
||||
var principal: HttpUrl?
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val TYPE_CALDAV = "caldav"
|
||||
const val TYPE_CARDDAV = "carddav"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
|
||||
@Dao
|
||||
interface ServiceDao {
|
||||
|
||||
@Query("SELECT * FROM service WHERE accountName=:accountName AND type=:type")
|
||||
fun getByAccountAndType(accountName: String, type: String): Service?
|
||||
|
||||
@Query("SELECT * FROM service WHERE accountName=:accountName AND type=:type")
|
||||
fun getLiveByAccountAndType(accountName: String, type: String): LiveData<Service?>
|
||||
|
||||
@Query("SELECT id FROM service WHERE accountName=:accountName")
|
||||
fun getIdsByAccount(accountName: String): List<Long>
|
||||
|
||||
@Query("SELECT id FROM service WHERE accountName=:accountName AND type=:type")
|
||||
fun getIdByAccountAndType(accountName: String, type: String): LiveData<Long>
|
||||
|
||||
@Query("SELECT type, id FROM service WHERE accountName=:accountName")
|
||||
fun getServiceTypeAndIdsByAccount(accountName: String): LiveData<List<ServiceTypeAndId>>
|
||||
|
||||
@Query("SELECT * FROM service WHERE id=:id")
|
||||
fun get(id: Long): Service?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertOrReplace(service: Service): Long
|
||||
|
||||
@Query("DELETE FROM service")
|
||||
fun deleteAll()
|
||||
|
||||
@Query("DELETE FROM service WHERE accountName NOT IN (:accountNames)")
|
||||
fun deleteExceptAccounts(accountNames: Array<String>)
|
||||
|
||||
@Query("UPDATE service SET accountName=:newName WHERE accountName=:oldName")
|
||||
fun renameAccount(oldName: String, newName: String)
|
||||
|
||||
}
|
||||
|
||||
data class ServiceTypeAndId(
|
||||
@ColumnInfo(name = "type") val type: String,
|
||||
@ColumnInfo(name = "id") val id: Long
|
||||
)
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import at.bitfire.dav4jvm.property.webdav.SyncToken
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
data class SyncState(
|
||||
val type: Type,
|
||||
val value: String,
|
||||
|
||||
/**
|
||||
* Whether this sync state occurred during an initial sync as described
|
||||
* in RFC 6578, which means the initial sync is not complete yet.
|
||||
*/
|
||||
var initialSync: Boolean? = null
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_TYPE = "type"
|
||||
private const val KEY_VALUE = "value"
|
||||
private const val KEY_INITIAL_SYNC = "initialSync"
|
||||
|
||||
fun fromString(s: String?): SyncState? {
|
||||
if (s == null)
|
||||
return null
|
||||
|
||||
return try {
|
||||
val json = JSONObject(s)
|
||||
SyncState(
|
||||
Type.valueOf(json.getString(KEY_TYPE)),
|
||||
json.getString(KEY_VALUE),
|
||||
try { json.getBoolean(KEY_INITIAL_SYNC) } catch(e: JSONException) { null }
|
||||
)
|
||||
} catch (e: JSONException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun fromSyncToken(token: SyncToken, initialSync: Boolean? = null) =
|
||||
SyncState(Type.SYNC_TOKEN, requireNotNull(token.token), initialSync)
|
||||
|
||||
}
|
||||
|
||||
enum class Type { CTAG, SYNC_TOKEN }
|
||||
|
||||
override fun toString(): String {
|
||||
val json = JSONObject()
|
||||
json.put(KEY_TYPE, type.name)
|
||||
json.put(KEY_VALUE, value)
|
||||
initialSync?.let { json.put(KEY_INITIAL_SYNC, it) }
|
||||
return json.toString()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "syncstats",
|
||||
foreignKeys = [
|
||||
ForeignKey(childColumns = arrayOf("collectionId"), entity = Collection::class, parentColumns = arrayOf("id"), onDelete = ForeignKey.CASCADE)
|
||||
],
|
||||
indices = [
|
||||
Index("collectionId", "authority", unique = true),
|
||||
]
|
||||
)
|
||||
data class SyncStats(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Long,
|
||||
|
||||
val collectionId: Long,
|
||||
val authority: String,
|
||||
|
||||
var lastSync: Long
|
||||
)
|
||||
@@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
|
||||
@Dao
|
||||
interface SyncStatsDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertOrReplace(syncStats: SyncStats)
|
||||
|
||||
@Query("SELECT * FROM syncstats WHERE collectionId=:id")
|
||||
fun getLiveByCollectionId(id: Long): LiveData<List<SyncStats>>
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.provider.DocumentsContract.Document
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import at.bitfire.davdroid.util.DavUtils.MEDIA_TYPE_OCTET_STREAM
|
||||
import at.bitfire.davdroid.webdav.DocumentState
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import java.io.FileNotFoundException
|
||||
import java.time.Instant
|
||||
|
||||
@Entity(
|
||||
tableName = "webdav_document",
|
||||
foreignKeys = [
|
||||
ForeignKey(entity = WebDavMount::class, parentColumns = ["id"], childColumns = ["mountId"], onDelete = ForeignKey.CASCADE),
|
||||
ForeignKey(entity = WebDavDocument::class, parentColumns = ["id"], childColumns = ["parentId"], onDelete = ForeignKey.CASCADE)
|
||||
],
|
||||
indices = [
|
||||
Index("mountId", "parentId", "name", unique = true)
|
||||
]
|
||||
)
|
||||
data class WebDavDocument(
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0,
|
||||
|
||||
/** refers to the [WebDavMount] the document belongs to */
|
||||
val mountId: Long,
|
||||
|
||||
/** refers to parent document (*null* when this document is a root document) */
|
||||
var parentId: Long?,
|
||||
|
||||
/** file name (without any slashes) */
|
||||
var name: String,
|
||||
var isDirectory: Boolean = false,
|
||||
|
||||
var displayName: String? = null,
|
||||
var mimeType: MediaType? = null,
|
||||
var eTag: String? = null,
|
||||
var lastModified: Long? = null,
|
||||
var size: Long? = null,
|
||||
|
||||
var mayBind: Boolean? = null,
|
||||
var mayUnbind: Boolean? = null,
|
||||
var mayWriteContent: Boolean? = null,
|
||||
|
||||
var quotaAvailable: Long? = null,
|
||||
var quotaUsed: Long? = null
|
||||
|
||||
) {
|
||||
|
||||
fun cacheKey(): CacheKey? {
|
||||
if (eTag != null || lastModified != null)
|
||||
return CacheKey(id, DocumentState(eTag, lastModified?.let { ts -> Instant.ofEpochMilli(ts) }))
|
||||
return null
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
fun toBundle(parent: WebDavDocument?): Bundle {
|
||||
if (parent?.isDirectory == false)
|
||||
throw IllegalArgumentException("Parent must be a directory")
|
||||
|
||||
val bundle = Bundle()
|
||||
bundle.putString(Document.COLUMN_DOCUMENT_ID, id.toString())
|
||||
bundle.putString(Document.COLUMN_DISPLAY_NAME, name)
|
||||
|
||||
displayName?.let { bundle.putString(Document.COLUMN_SUMMARY, it) }
|
||||
size?.let { bundle.putLong(Document.COLUMN_SIZE, it) }
|
||||
lastModified?.let { bundle.putLong(Document.COLUMN_LAST_MODIFIED, it) }
|
||||
|
||||
// see RFC 3744 appendix B for required privileges for the various operations
|
||||
var flags = Document.FLAG_SUPPORTS_COPY
|
||||
if (isDirectory) {
|
||||
bundle.putString(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR)
|
||||
if (mayBind != false)
|
||||
flags += Document.FLAG_DIR_SUPPORTS_CREATE
|
||||
} else {
|
||||
val reportedMimeType = mimeType ?:
|
||||
MimeTypeMap.getSingleton().getMimeTypeFromExtension(
|
||||
MimeTypeMap.getFileExtensionFromUrl(name)
|
||||
)?.toMediaTypeOrNull() ?:
|
||||
MEDIA_TYPE_OCTET_STREAM
|
||||
|
||||
bundle.putString(Document.COLUMN_MIME_TYPE, reportedMimeType.toString())
|
||||
if (mimeType?.type == "image")
|
||||
flags += Document.FLAG_SUPPORTS_THUMBNAIL
|
||||
if (mayWriteContent != false)
|
||||
flags += Document.FLAG_SUPPORTS_WRITE
|
||||
}
|
||||
if (parent?.mayUnbind != false)
|
||||
flags += Document.FLAG_SUPPORTS_DELETE or
|
||||
Document.FLAG_SUPPORTS_MOVE or
|
||||
Document.FLAG_SUPPORTS_RENAME
|
||||
bundle.putInt(Document.COLUMN_FLAGS, flags)
|
||||
|
||||
return bundle
|
||||
}
|
||||
|
||||
fun toHttpUrl(db: AppDatabase): HttpUrl {
|
||||
val mount = db.webDavMountDao().getById(mountId)
|
||||
|
||||
val segments = mutableListOf(name)
|
||||
var parentIter = parentId
|
||||
while (parentIter != null) {
|
||||
val parent = db.webDavDocumentDao().get(parentIter) ?: throw FileNotFoundException()
|
||||
segments += parent.name
|
||||
parentIter = parent.parentId
|
||||
}
|
||||
|
||||
val builder = mount.url.newBuilder()
|
||||
for (segment in segments.reversed())
|
||||
builder.addPathSegment(segment)
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents a WebDAV document in a given state (with a given ETag/Last-Modified).
|
||||
*/
|
||||
data class CacheKey(
|
||||
val docId: Long,
|
||||
val documentState: DocumentState
|
||||
)
|
||||
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
|
||||
@Dao
|
||||
interface WebDavDocumentDao {
|
||||
|
||||
@Query("SELECT * FROM webdav_document WHERE id=:id")
|
||||
fun get(id: Long): WebDavDocument?
|
||||
|
||||
@Query("SELECT * FROM webdav_document WHERE mountId=:mountId AND (parentId=:parentId OR (parentId IS NULL AND :parentId IS NULL)) AND name=:name")
|
||||
fun getByParentAndName(mountId: Long, parentId: Long?, name: String): WebDavDocument?
|
||||
|
||||
@Query("SELECT * FROM webdav_document WHERE parentId=:parentId")
|
||||
fun getChildren(parentId: Long): List<WebDavDocument>
|
||||
|
||||
@Query("SELECT * FROM webdav_document WHERE parentId IS NULL")
|
||||
fun getRootsLive(): LiveData<List<WebDavDocument>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertOrReplace(document: WebDavDocument): Long
|
||||
|
||||
@Query("DELETE FROM webdav_document WHERE parentId=:parentId")
|
||||
fun removeChildren(parentId: Long)
|
||||
|
||||
@Insert
|
||||
fun insert(document: WebDavDocument): Long
|
||||
|
||||
@Update
|
||||
fun update(document: WebDavDocument)
|
||||
|
||||
@Delete
|
||||
fun delete(document: WebDavDocument)
|
||||
|
||||
|
||||
// complex operations
|
||||
|
||||
/**
|
||||
* Tries to insert new row, but updates existing row if already present.
|
||||
* This method preserves the primary key, as opposed to using "@Insert(onConflict = OnConflictStrategy.REPLACE)"
|
||||
* which will create a new row with incremented ID and thus breaks entity relationships!
|
||||
*
|
||||
* @return ID of the row, that has been inserted or updated. -1 If the insert fails due to other reasons.
|
||||
*/
|
||||
@Transaction
|
||||
fun insertOrUpdate(document: WebDavDocument): Long {
|
||||
val parentId = document.parentId
|
||||
?: return insert(document)
|
||||
val existingDocument = getByParentAndName(document.mountId, parentId, document.name)
|
||||
?: return insert(document)
|
||||
update(document.copy(id = existingDocument.id))
|
||||
return existingDocument.id
|
||||
}
|
||||
|
||||
@Transaction
|
||||
fun getOrCreateRoot(mount: WebDavMount): WebDavDocument {
|
||||
getByParentAndName(mount.id, null, "")?.let { existing ->
|
||||
return existing
|
||||
}
|
||||
|
||||
val newDoc = WebDavDocument(
|
||||
mountId = mount.id,
|
||||
parentId = null,
|
||||
name = "",
|
||||
isDirectory = true,
|
||||
displayName = mount.name
|
||||
)
|
||||
val id = insertOrReplace(newDoc)
|
||||
newDoc.id = id
|
||||
return newDoc
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
@Entity(tableName = "webdav_mount")
|
||||
data class WebDavMount(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0,
|
||||
|
||||
/** display name of the WebDAV mount */
|
||||
var name: String,
|
||||
|
||||
/** URL of the WebDAV service, including trailing slash */
|
||||
var url: HttpUrl
|
||||
|
||||
// credentials are stored using CredentialsStore
|
||||
|
||||
)
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
|
||||
@Dao
|
||||
interface WebDavMountDao {
|
||||
|
||||
@Delete
|
||||
fun delete(mount: WebDavMount)
|
||||
|
||||
@Query("SELECT * FROM webdav_mount ORDER BY name, url")
|
||||
fun getAll(): List<WebDavMount>
|
||||
|
||||
@Query("SELECT * FROM webdav_mount ORDER BY name, url")
|
||||
fun getAllLive(): LiveData<List<WebDavMount>>
|
||||
|
||||
@Query("SELECT * FROM webdav_mount WHERE id=:id")
|
||||
fun getById(id: Long): WebDavMount
|
||||
|
||||
@Insert
|
||||
fun insert(mount: WebDavMount): Long
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.log
|
||||
|
||||
import android.util.Log
|
||||
|
||||
import org.apache.commons.lang3.math.NumberUtils
|
||||
|
||||
import java.util.logging.Handler
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.LogRecord
|
||||
|
||||
object LogcatHandler: Handler() {
|
||||
|
||||
private const val MAX_LINE_LENGTH = 3000
|
||||
|
||||
init {
|
||||
formatter = PlainTextFormatter.LOGCAT
|
||||
level = Level.ALL
|
||||
}
|
||||
|
||||
override fun publish(r: LogRecord) {
|
||||
val text = formatter.format(r)
|
||||
val level = r.level.intValue()
|
||||
|
||||
val end = text.length
|
||||
var pos = 0
|
||||
while (pos < end) {
|
||||
val line = text.substring(pos, NumberUtils.min(pos + MAX_LINE_LENGTH, end))
|
||||
when {
|
||||
level >= Level.SEVERE.intValue() -> Log.e(r.loggerName, line)
|
||||
level >= Level.WARNING.intValue() -> Log.w(r.loggerName, line)
|
||||
level >= Level.CONFIG.intValue() -> Log.i(r.loggerName, line)
|
||||
level >= Level.FINER.intValue() -> Log.d(r.loggerName, line)
|
||||
else -> Log.v(r.loggerName, line)
|
||||
}
|
||||
pos += MAX_LINE_LENGTH
|
||||
}
|
||||
}
|
||||
|
||||
override fun flush() {}
|
||||
override fun close() {}
|
||||
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.log
|
||||
|
||||
import android.app.Application
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Process
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.ui.AppSettingsActivity
|
||||
import at.bitfire.davdroid.ui.DebugInfoActivity
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.Date
|
||||
import java.util.logging.FileHandler
|
||||
import java.util.logging.Level
|
||||
|
||||
object Logger : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private const val LOGGER_NAME = "davx5"
|
||||
const val LOG_TO_FILE = "log_to_file"
|
||||
|
||||
val log: java.util.logging.Logger = java.util.logging.Logger.getLogger(LOGGER_NAME)
|
||||
|
||||
private lateinit var context: Application
|
||||
private lateinit var preferences: SharedPreferences
|
||||
|
||||
|
||||
fun initialize(app: Application) {
|
||||
context = app
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
preferences.registerOnSharedPreferenceChangeListener(this)
|
||||
|
||||
reinitialize()
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
||||
if (key == LOG_TO_FILE) {
|
||||
log.info("Logging settings changed; re-initializing logger")
|
||||
reinitialize()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun reinitialize() {
|
||||
val logToFile = preferences.getBoolean(LOG_TO_FILE, false)
|
||||
val logVerbose = logToFile || BuildConfig.DEBUG || Log.isLoggable(log.name, Log.DEBUG)
|
||||
|
||||
log.info("Verbose logging: $logVerbose; to file: $logToFile")
|
||||
|
||||
// set logging level according to preferences
|
||||
val rootLogger = java.util.logging.Logger.getLogger("")
|
||||
rootLogger.level = if (logVerbose) Level.ALL else Level.INFO
|
||||
|
||||
// reset all handlers and add our own logcat handler
|
||||
rootLogger.useParentHandlers = false
|
||||
rootLogger.handlers.forEach { handler ->
|
||||
rootLogger.removeHandler(handler)
|
||||
if (handler is FileHandler) // gracefully close previous verbose-logging FileHandlers
|
||||
handler.close()
|
||||
}
|
||||
rootLogger.addHandler(LogcatHandler)
|
||||
|
||||
val nm = NotificationManagerCompat.from(context)
|
||||
// log to external file according to preferences
|
||||
if (logToFile) {
|
||||
val logFile = getDebugLogFile() ?: return log.warning("Log file could not be retrieved.")
|
||||
if (logFile.createNewFile())
|
||||
logFile.writeText("Log file created at ${Date()}; PID ${Process.myPid()}; UID ${Process.myUid()}\n")
|
||||
|
||||
try {
|
||||
val fileHandler = FileHandler(logFile.toString(), true).apply {
|
||||
formatter = PlainTextFormatter.DEFAULT
|
||||
}
|
||||
rootLogger.addHandler(fileHandler)
|
||||
log.info("Now logging to file: $logFile")
|
||||
|
||||
val builder = NotificationUtils.newBuilder(context, NotificationUtils.CHANNEL_DEBUG)
|
||||
builder .setSmallIcon(R.drawable.ic_sd_card_notify)
|
||||
.setContentTitle(context.getString(R.string.app_settings_logging))
|
||||
.setCategory(NotificationCompat.CATEGORY_STATUS)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setContentText(context.getString(R.string.logging_notification_text, context.getString(R.string.app_name)))
|
||||
.setOngoing(true)
|
||||
|
||||
val shareIntent = DebugInfoActivity.IntentBuilder(context)
|
||||
.newTask()
|
||||
.share()
|
||||
val pendingShare = PendingIntent.getActivity(context, 0, shareIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
builder.addAction(NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_share,
|
||||
context.getString(R.string.logging_notification_view_share),
|
||||
pendingShare
|
||||
).build())
|
||||
|
||||
val prefIntent = Intent(context, AppSettingsActivity::class.java)
|
||||
prefIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
val pendingPref = PendingIntent.getActivity(context, 0, prefIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
builder.addAction(NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_settings,
|
||||
context.getString(R.string.logging_notification_disable),
|
||||
pendingPref
|
||||
).build())
|
||||
|
||||
nm.notifyIfPossible(NotificationUtils.NOTIFY_VERBOSE_LOGGING, builder.build())
|
||||
} catch(e: IOException) {
|
||||
log.log(Level.SEVERE, "Couldn't create log file", e)
|
||||
Toast.makeText(context, context.getString(R.string.logging_couldnt_create_file), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} else {
|
||||
// verbose logging is disabled -> cancel notification and remove old logs
|
||||
nm.cancel(NotificationUtils.NOTIFY_VERBOSE_LOGGING)
|
||||
debugDir()?.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates (when necessary) and returns the directory where all the debug files (such as log files) are stored.
|
||||
* Must match the contents of `res/xml/debug.paths.xml`.
|
||||
*
|
||||
* @return The directory where all debug info are stored, or `null` if the directory couldn't be created successfully.
|
||||
*/
|
||||
fun debugDir(): File? {
|
||||
val dir = File(context.filesDir, "debug")
|
||||
if (dir.exists() && dir.isDirectory)
|
||||
return dir
|
||||
|
||||
if (dir.mkdir())
|
||||
return dir
|
||||
|
||||
Toast.makeText(context, context.getString(R.string.logging_couldnt_create_file), Toast.LENGTH_LONG).show()
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* The file (in [debugDir]) where verbose logs are stored.
|
||||
*
|
||||
* @return The file where verbose logs are stored, or `null` if there's no [debugDir].
|
||||
*/
|
||||
fun getDebugLogFile(): File? {
|
||||
val logDir = debugDir() ?: return null
|
||||
return File(logDir, "davx5-log.txt")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.log
|
||||
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
import org.apache.commons.lang3.time.DateFormatUtils
|
||||
import java.util.Locale
|
||||
import java.util.logging.Formatter
|
||||
import java.util.logging.LogRecord
|
||||
|
||||
class PlainTextFormatter private constructor(
|
||||
private val logcat: Boolean
|
||||
): Formatter() {
|
||||
|
||||
companion object {
|
||||
val LOGCAT = PlainTextFormatter(true)
|
||||
val DEFAULT = PlainTextFormatter(false)
|
||||
|
||||
const val MAX_MESSAGE_LENGTH = 20000
|
||||
}
|
||||
|
||||
override fun format(r: LogRecord): String {
|
||||
val builder = StringBuilder()
|
||||
|
||||
if (!logcat)
|
||||
builder .append(DateFormatUtils.format(r.millis, "yyyy-MM-dd HH:mm:ss", Locale.ROOT))
|
||||
.append(" ").append(r.threadID).append(" ")
|
||||
|
||||
val className = shortClassName(r.sourceClassName)
|
||||
if (className != r.loggerName)
|
||||
builder.append("[").append(className).append("] ")
|
||||
|
||||
builder.append(StringUtils.abbreviate(r.message, MAX_MESSAGE_LENGTH))
|
||||
|
||||
r.thrown?.let {
|
||||
builder .append("\nEXCEPTION ")
|
||||
.append(ExceptionUtils.getStackTrace(it))
|
||||
}
|
||||
|
||||
r.parameters?.let {
|
||||
for ((idx, param) in it.withIndex())
|
||||
builder.append("\n\tPARAMETER #").append(idx).append(" = ").append(param)
|
||||
}
|
||||
|
||||
if (!logcat)
|
||||
builder.append("\n")
|
||||
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
private fun shortClassName(className: String) = className
|
||||
.replace(Regex("^at\\.bitfire\\.(dav|cert4an|dav4an|ical4an|vcard4an)droid\\."), "")
|
||||
.replace(Regex("\\$.*$"), "")
|
||||
|
||||
}
|
||||
Loaded 100 of 651 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user