From d56175652c2f4fc4a3282f343b5c02743555e2df Mon Sep 17 00:00:00 2001 From: R Hirner Date: Sat, 20 Dec 2014 20:21:46 +0100 Subject: [PATCH] Migrated to Android Studio/gradle * moved all dependencies to gradle instead of shipping .jar files in the app/lib directory * switched to official Android port of HttpClient instead of httpclientandroidlib * new .gitignore and project files --- .gitignore | 86 ++ app/.gitignore | 1 + app/build.gradle | 62 + app/lint.xml | 5 + .../davdroid/resource/test/ContactTest.java | 65 ++ .../davdroid/resource/test/EventTest.java | 127 +++ .../resource/test/LocalCalendarTest.java | 154 +++ .../syncadapter/DavResourceFinderTest.java | 69 ++ .../bitfire/davdroid/test/ArrayUtilsTest.java | 39 + .../at/bitfire/davdroid/test/Constants.java | 19 + .../at/bitfire/davdroid/test/ContactTest.java | 62 + .../bitfire/davdroid/test/URLUtilsTest.java | 61 + .../webdav/DavRedirectStrategyTest.java | 73 ++ .../webdav/TlsSniSocketFactoryTest.java | 27 + .../davdroid/webdav/WebDavResourceTest.java | 233 ++++ .../res/drawable-hdpi/ic_launcher.png | Bin 0 -> 9397 bytes .../res/drawable-ldpi/ic_launcher.png | Bin 0 -> 2729 bytes .../res/drawable-mdpi/ic_launcher.png | Bin 0 -> 5237 bytes .../res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 14383 bytes app/src/androidTest/res/values/strings.xml | 6 + app/src/main/AndroidManifest.xml | 79 ++ .../java/at/bitfire/davdroid/ArrayUtils.java | 33 + .../java/at/bitfire/davdroid/Constants.java | 18 + .../at/bitfire/davdroid/MainActivity.java | 82 ++ .../java/at/bitfire/davdroid/URIUtils.java | 53 + .../davdroid/resource/CalDavCalendar.java | 37 + .../davdroid/resource/CardDavAddressBook.java | 37 + .../at/bitfire/davdroid/resource/Contact.java | 410 +++++++ .../davdroid/resource/DavResourceFinder.java | 298 +++++ .../at/bitfire/davdroid/resource/Event.java | 368 ++++++ .../resource/InvalidResourceException.java | 20 + .../davdroid/resource/LocalAddressBook.java | 1007 +++++++++++++++++ .../davdroid/resource/LocalCalendar.java | 612 ++++++++++ .../davdroid/resource/LocalCollection.java | 361 ++++++ .../resource/LocalStorageException.java | 31 + .../resource/RecordNotFoundException.java | 28 + .../davdroid/resource/RemoteCollection.java | 180 +++ .../bitfire/davdroid/resource/Resource.java | 46 + .../bitfire/davdroid/resource/ServerInfo.java | 68 ++ .../AccountAuthenticatorService.java | 86 ++ .../syncadapter/AccountDetailsFragment.java | 147 +++ .../davdroid/syncadapter/AccountSettings.java | 183 +++ .../syncadapter/AddAccountActivity.java | 46 + .../CalendarsSyncAdapterService.java | 81 ++ .../ContactsSyncAdapterService.java | 82 ++ .../davdroid/syncadapter/DavSyncAdapter.java | 166 +++ .../syncadapter/GeneralSettingsActivity.java | 54 + .../syncadapter/LoginEmailFragment.java | 111 ++ .../syncadapter/LoginTypeFragment.java | 57 + .../syncadapter/LoginURLFragment.java | 153 +++ .../QueryServerDialogFragment.java | 129 +++ .../syncadapter/SelectCollectionsAdapter.java | 161 +++ .../SelectCollectionsFragment.java | 127 +++ .../davdroid/syncadapter/SyncManager.java | 214 ++++ .../syncadapter/WebDavResourceAdapter.java | 62 + .../webdav/DavAddressbookMultiget.java | 21 + .../davdroid/webdav/DavCalendarMultiget.java | 21 + .../bitfire/davdroid/webdav/DavException.java | 25 + .../at/bitfire/davdroid/webdav/DavHref.java | 26 + .../davdroid/webdav/DavHttpClient.java | 70 ++ .../webdav/DavHttpRequestRetryHandler.java | 35 + .../webdav/DavIncapableException.java | 18 + .../bitfire/davdroid/webdav/DavMultiget.java | 48 + .../davdroid/webdav/DavMultistatus.java | 21 + .../webdav/DavNoContentException.java | 18 + .../webdav/DavNoMultiStatusException.java | 18 + .../at/bitfire/davdroid/webdav/DavProp.java | 211 ++++ .../bitfire/davdroid/webdav/DavPropfind.java | 19 + .../bitfire/davdroid/webdav/DavPropstat.java | 20 + .../davdroid/webdav/DavRedirectStrategy.java | 105 ++ .../bitfire/davdroid/webdav/DavResponse.java | 25 + .../davdroid/webdav/HttpException.java | 26 + .../bitfire/davdroid/webdav/HttpPropfind.java | 102 ++ .../bitfire/davdroid/webdav/HttpReport.java | 46 + .../webdav/NotAuthorizedException.java | 19 + .../davdroid/webdav/NotFoundException.java | 18 + .../webdav/PreconditionFailedException.java | 18 + .../davdroid/webdav/TlsSniSocketFactory.java | 186 +++ .../davdroid/webdav/WebDavResource.java | 581 ++++++++++ app/src/main/java/ical4j.properties | 6 + .../main/res/drawable-hdpi/addressbook.png | Bin 0 -> 1212 bytes .../alerts_and_states_warning.png | Bin 0 -> 878 bytes app/src/main/res/drawable-hdpi/calendar.png | Bin 0 -> 1429 bytes .../res/drawable-hdpi/extra_actions_about.png | Bin 0 -> 1107 bytes .../drawable-hdpi/ic_action_new_account.png | Bin 0 -> 962 bytes .../res/drawable-hdpi/ic_action_new_event.png | Bin 0 -> 409 bytes .../main/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 5274 bytes .../main/res/drawable-hdpi/ic_read_only.png | Bin 0 -> 1438 bytes .../res/drawable-hdpi/navigation_accept.png | Bin 0 -> 1320 bytes .../res/drawable-hdpi/navigation_forward.png | Bin 0 -> 1304 bytes .../res/drawable-hdpi/show_sync_settings.png | Bin 0 -> 1550 bytes .../main/res/drawable-hdpi/view_website.png | Bin 0 -> 1917 bytes .../main/res/drawable-mdpi/addressbook.png | Bin 0 -> 804 bytes .../alerts_and_states_warning.png | Bin 0 -> 613 bytes app/src/main/res/drawable-mdpi/calendar.png | Bin 0 -> 1393 bytes .../res/drawable-mdpi/extra_actions_about.png | Bin 0 -> 671 bytes .../drawable-mdpi/ic_action_new_account.png | Bin 0 -> 666 bytes .../res/drawable-mdpi/ic_action_new_event.png | Bin 0 -> 384 bytes .../main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 3272 bytes .../main/res/drawable-mdpi/ic_read_only.png | Bin 0 -> 868 bytes .../res/drawable-mdpi/navigation_accept.png | Bin 0 -> 1197 bytes .../res/drawable-mdpi/navigation_forward.png | Bin 0 -> 1221 bytes .../res/drawable-mdpi/show_sync_settings.png | Bin 0 -> 910 bytes .../main/res/drawable-mdpi/view_website.png | Bin 0 -> 1144 bytes .../main/res/drawable-xhdpi/addressbook.png | Bin 0 -> 1251 bytes .../alerts_and_states_warning.png | Bin 0 -> 1020 bytes app/src/main/res/drawable-xhdpi/calendar.png | Bin 0 -> 1677 bytes .../drawable-xhdpi/extra_actions_about.png | Bin 0 -> 1455 bytes .../drawable-xhdpi/ic_action_new_account.png | Bin 0 -> 1049 bytes .../drawable-xhdpi/ic_action_new_event.png | Bin 0 -> 481 bytes .../main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 7765 bytes .../main/res/drawable-xhdpi/ic_read_only.png | Bin 0 -> 1895 bytes .../res/drawable-xhdpi/navigation_accept.png | Bin 0 -> 1546 bytes .../res/drawable-xhdpi/navigation_forward.png | Bin 0 -> 1458 bytes .../res/drawable-xhdpi/show_sync_settings.png | Bin 0 -> 2038 bytes .../main/res/drawable-xhdpi/view_website.png | Bin 0 -> 2638 bytes .../main/res/drawable-xxhdpi/addressbook.png | Bin 0 -> 2817 bytes .../alerts_and_states_warning.png | Bin 0 -> 912 bytes app/src/main/res/drawable-xxhdpi/calendar.png | Bin 0 -> 1879 bytes .../drawable-xxhdpi/extra_actions_about.png | Bin 0 -> 1436 bytes .../drawable-xxhdpi/ic_action_new_account.png | Bin 0 -> 920 bytes .../drawable-xxhdpi/ic_action_new_event.png | Bin 0 -> 530 bytes .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 12150 bytes .../main/res/drawable-xxhdpi/ic_read_only.png | Bin 0 -> 2037 bytes .../res/drawable-xxhdpi/navigation_accept.png | Bin 0 -> 649 bytes .../drawable-xxhdpi/navigation_forward.png | Bin 0 -> 578 bytes .../drawable-xxhdpi/show_sync_settings.png | Bin 0 -> 2745 bytes .../main/res/drawable-xxhdpi/view_website.png | Bin 0 -> 2703 bytes app/src/main/res/layout/account_details.xml | 53 + app/src/main/res/layout/activity_main.xml | 29 + app/src/main/res/layout/add_account.xml | 7 + .../main/res/layout/address_books_heading.xml | 20 + app/src/main/res/layout/calendars_heading.xml | 21 + app/src/main/res/layout/login_email.xml | 52 + app/src/main/res/layout/login_type.xml | 38 + app/src/main/res/layout/login_url.xml | 89 ++ app/src/main/res/layout/query_server.xml | 23 + .../res/layout/select_collections_header.xml | 18 + app/src/main/res/menu/account_details.xml | 11 + app/src/main/res/menu/add_account.xml | 10 + app/src/main/res/menu/debug_settings.xml | 4 + app/src/main/res/menu/main_activity.xml | 7 + app/src/main/res/menu/only_next.xml | 11 + app/src/main/res/values-ca/strings.xml | 142 +++ app/src/main/res/values-cs/strings.xml | 141 +++ app/src/main/res/values-de/strings.xml | 138 +++ app/src/main/res/values-es/strings.xml | 86 ++ app/src/main/res/values-fr/strings.xml | 93 ++ app/src/main/res/values-hu/strings.xml | 147 +++ app/src/main/res/values-sr/strings.xml | 136 +++ app/src/main/res/values-zh-rcn/strings.xml | 126 +++ app/src/main/res/values/strings.xml | 140 +++ app/src/main/res/values/styles.xml | 26 + .../main/res/xml/account_authenticator.xml | 6 + app/src/main/res/xml/account_prefs.xml | 21 + app/src/main/res/xml/contacts.xml | 86 ++ app/src/main/res/xml/general_settings.xml | 11 + app/src/main/res/xml/sync_calendars.xml | 7 + app/src/main/res/xml/sync_contacts.xml | 7 + build.gradle | 16 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 49896 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 +++ gradlew.bat | 90 ++ import-summary.txt | 741 ++++++++++++ settings.gradle | 1 + 166 files changed, 11143 insertions(+) create mode 100644 .gitignore create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/lint.xml create mode 100644 app/src/androidTest/java/at/bitfire/davdroid/resource/test/ContactTest.java create mode 100644 app/src/androidTest/java/at/bitfire/davdroid/resource/test/EventTest.java create mode 100644 app/src/androidTest/java/at/bitfire/davdroid/resource/test/LocalCalendarTest.java create mode 100644 app/src/androidTest/java/at/bitfire/davdroid/syncadapter/DavResourceFinderTest.java create mode 100644 app/src/androidTest/java/at/bitfire/davdroid/test/ArrayUtilsTest.java create mode 100644 app/src/androidTest/java/at/bitfire/davdroid/test/Constants.java create mode 100644 app/src/androidTest/java/at/bitfire/davdroid/test/ContactTest.java create mode 100644 app/src/androidTest/java/at/bitfire/davdroid/test/URLUtilsTest.java create mode 100644 app/src/androidTest/java/at/bitfire/davdroid/webdav/DavRedirectStrategyTest.java create mode 100644 app/src/androidTest/java/at/bitfire/davdroid/webdav/TlsSniSocketFactoryTest.java create mode 100644 app/src/androidTest/java/at/bitfire/davdroid/webdav/WebDavResourceTest.java create mode 100644 app/src/androidTest/res/drawable-hdpi/ic_launcher.png create mode 100644 app/src/androidTest/res/drawable-ldpi/ic_launcher.png create mode 100644 app/src/androidTest/res/drawable-mdpi/ic_launcher.png create mode 100644 app/src/androidTest/res/drawable-xhdpi/ic_launcher.png create mode 100644 app/src/androidTest/res/values/strings.xml create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/at/bitfire/davdroid/ArrayUtils.java create mode 100644 app/src/main/java/at/bitfire/davdroid/Constants.java create mode 100644 app/src/main/java/at/bitfire/davdroid/MainActivity.java create mode 100644 app/src/main/java/at/bitfire/davdroid/URIUtils.java create mode 100644 app/src/main/java/at/bitfire/davdroid/resource/CalDavCalendar.java create mode 100644 app/src/main/java/at/bitfire/davdroid/resource/CardDavAddressBook.java create mode 100644 app/src/main/java/at/bitfire/davdroid/resource/Contact.java create mode 100644 app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java create mode 100644 app/src/main/java/at/bitfire/davdroid/resource/Event.java create mode 100644 app/src/main/java/at/bitfire/davdroid/resource/InvalidResourceException.java create mode 100644 app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.java create mode 100644 app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java create mode 100644 app/src/main/java/at/bitfire/davdroid/resource/LocalCollection.java create mode 100644 app/src/main/java/at/bitfire/davdroid/resource/LocalStorageException.java create mode 100644 app/src/main/java/at/bitfire/davdroid/resource/RecordNotFoundException.java create mode 100644 app/src/main/java/at/bitfire/davdroid/resource/RemoteCollection.java create mode 100644 app/src/main/java/at/bitfire/davdroid/resource/Resource.java create mode 100644 app/src/main/java/at/bitfire/davdroid/resource/ServerInfo.java create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/AccountAuthenticatorService.java create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/AccountDetailsFragment.java create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/AccountSettings.java create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/AddAccountActivity.java create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/DavSyncAdapter.java create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/GeneralSettingsActivity.java create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/LoginEmailFragment.java create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/LoginTypeFragment.java create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/LoginURLFragment.java create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/QueryServerDialogFragment.java create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/SelectCollectionsAdapter.java create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/SelectCollectionsFragment.java create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java create mode 100644 app/src/main/java/at/bitfire/davdroid/syncadapter/WebDavResourceAdapter.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavAddressbookMultiget.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavCalendarMultiget.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavException.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavHref.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavHttpClient.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavHttpRequestRetryHandler.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavIncapableException.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavMultiget.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavMultistatus.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavNoContentException.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavNoMultiStatusException.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavProp.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavPropfind.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavPropstat.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavRedirectStrategy.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/DavResponse.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/HttpException.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/HttpPropfind.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/HttpReport.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/NotAuthorizedException.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/NotFoundException.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/PreconditionFailedException.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/TlsSniSocketFactory.java create mode 100644 app/src/main/java/at/bitfire/davdroid/webdav/WebDavResource.java create mode 100644 app/src/main/java/ical4j.properties create mode 100644 app/src/main/res/drawable-hdpi/addressbook.png create mode 100644 app/src/main/res/drawable-hdpi/alerts_and_states_warning.png create mode 100644 app/src/main/res/drawable-hdpi/calendar.png create mode 100644 app/src/main/res/drawable-hdpi/extra_actions_about.png create mode 100644 app/src/main/res/drawable-hdpi/ic_action_new_account.png create mode 100644 app/src/main/res/drawable-hdpi/ic_action_new_event.png create mode 100644 app/src/main/res/drawable-hdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-hdpi/ic_read_only.png create mode 100644 app/src/main/res/drawable-hdpi/navigation_accept.png create mode 100644 app/src/main/res/drawable-hdpi/navigation_forward.png create mode 100644 app/src/main/res/drawable-hdpi/show_sync_settings.png create mode 100644 app/src/main/res/drawable-hdpi/view_website.png create mode 100644 app/src/main/res/drawable-mdpi/addressbook.png create mode 100644 app/src/main/res/drawable-mdpi/alerts_and_states_warning.png create mode 100644 app/src/main/res/drawable-mdpi/calendar.png create mode 100644 app/src/main/res/drawable-mdpi/extra_actions_about.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_new_account.png create mode 100644 app/src/main/res/drawable-mdpi/ic_action_new_event.png create mode 100644 app/src/main/res/drawable-mdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-mdpi/ic_read_only.png create mode 100644 app/src/main/res/drawable-mdpi/navigation_accept.png create mode 100644 app/src/main/res/drawable-mdpi/navigation_forward.png create mode 100644 app/src/main/res/drawable-mdpi/show_sync_settings.png create mode 100644 app/src/main/res/drawable-mdpi/view_website.png create mode 100644 app/src/main/res/drawable-xhdpi/addressbook.png create mode 100644 app/src/main/res/drawable-xhdpi/alerts_and_states_warning.png create mode 100644 app/src/main/res/drawable-xhdpi/calendar.png create mode 100644 app/src/main/res/drawable-xhdpi/extra_actions_about.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_new_account.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_action_new_event.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_read_only.png create mode 100644 app/src/main/res/drawable-xhdpi/navigation_accept.png create mode 100644 app/src/main/res/drawable-xhdpi/navigation_forward.png create mode 100644 app/src/main/res/drawable-xhdpi/show_sync_settings.png create mode 100644 app/src/main/res/drawable-xhdpi/view_website.png create mode 100644 app/src/main/res/drawable-xxhdpi/addressbook.png create mode 100644 app/src/main/res/drawable-xxhdpi/alerts_and_states_warning.png create mode 100644 app/src/main/res/drawable-xxhdpi/calendar.png create mode 100644 app/src/main/res/drawable-xxhdpi/extra_actions_about.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_new_account.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_new_event.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_read_only.png create mode 100644 app/src/main/res/drawable-xxhdpi/navigation_accept.png create mode 100644 app/src/main/res/drawable-xxhdpi/navigation_forward.png create mode 100644 app/src/main/res/drawable-xxhdpi/show_sync_settings.png create mode 100644 app/src/main/res/drawable-xxhdpi/view_website.png create mode 100644 app/src/main/res/layout/account_details.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/add_account.xml create mode 100644 app/src/main/res/layout/address_books_heading.xml create mode 100644 app/src/main/res/layout/calendars_heading.xml create mode 100644 app/src/main/res/layout/login_email.xml create mode 100644 app/src/main/res/layout/login_type.xml create mode 100644 app/src/main/res/layout/login_url.xml create mode 100644 app/src/main/res/layout/query_server.xml create mode 100644 app/src/main/res/layout/select_collections_header.xml create mode 100644 app/src/main/res/menu/account_details.xml create mode 100644 app/src/main/res/menu/add_account.xml create mode 100644 app/src/main/res/menu/debug_settings.xml create mode 100644 app/src/main/res/menu/main_activity.xml create mode 100644 app/src/main/res/menu/only_next.xml create mode 100644 app/src/main/res/values-ca/strings.xml create mode 100644 app/src/main/res/values-cs/strings.xml create mode 100644 app/src/main/res/values-de/strings.xml create mode 100644 app/src/main/res/values-es/strings.xml create mode 100644 app/src/main/res/values-fr/strings.xml create mode 100644 app/src/main/res/values-hu/strings.xml create mode 100644 app/src/main/res/values-sr/strings.xml create mode 100644 app/src/main/res/values-zh-rcn/strings.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/xml/account_authenticator.xml create mode 100644 app/src/main/res/xml/account_prefs.xml create mode 100644 app/src/main/res/xml/contacts.xml create mode 100644 app/src/main/res/xml/general_settings.xml create mode 100644 app/src/main/res/xml/sync_calendars.xml create mode 100644 app/src/main/res/xml/sync_contacts.xml create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 import-summary.txt create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..5d711e0f --- /dev/null +++ b/.gitignore @@ -0,0 +1,86 @@ +# Created by https://www.gitignore.io + +### Android ### +# Built application files +*.apk +*.ap_ + +# Files for the Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm + +*.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 + + +### Gradle ### +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..378eac25 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..f57f6341 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,62 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 21 + buildToolsVersion "21.1.2" + + defaultConfig { + applicationId "at.bitfire.davdroid" + minSdkVersion 14 + targetSdkVersion 21 + + testApplicationId "at.bitfire.davdroid.test" + testInstrumentationRunner "android.test.InstrumentationTestRunner" + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } + dexOptions { + } + + packagingOptions { + exclude 'META-INF/LICENSE.txt' + exclude 'META-INF/NOTICE.txt' + } +} + +dependencies { + // Apache Commons + compile 'commons-lang:commons-lang:2.6' + compile 'org.apache.commons:commons-io:1.3.2' + + // Lombok for useful @helpers + compile 'org.projectlombok:lombok:1.14.8' + + // ical4j for parsing/generating iCalendars + compile 'org.mnode.ical4j:ical4j:1.0.6' + + // ez-vcard for parsing/generating VCards + compile('com.googlecode.ez-vcard:ez-vcard:0.9.6') { + // hCard functionality not needed + exclude group: 'org.jsoup', module: 'jsoup' + exclude group: 'org.freemarker', module: 'freemarker' + // jCard functionality not needed + exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core' + } + + // dnsjava for querying SRV/TXT records + compile 'dnsjava:dnsjava:2.1.6' + + // HttpClient 4.3, Android flavour for WebDAV operations + compile 'org.apache.httpcomponents:httpclient-android:4.3.5' + + // SimpleXML for parsing and generating WebDAV messages + compile('org.simpleframework:simple-xml:2.7.1') { + exclude group: 'stax', module: 'stax-api' + exclude group: 'xpp3', module: 'xpp3' + } +} diff --git a/app/lint.xml b/app/lint.xml new file mode 100644 index 00000000..d191f74b --- /dev/null +++ b/app/lint.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/androidTest/java/at/bitfire/davdroid/resource/test/ContactTest.java b/app/src/androidTest/java/at/bitfire/davdroid/resource/test/ContactTest.java new file mode 100644 index 00000000..26ee6b1b --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/resource/test/ContactTest.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.resource.test; + +import java.io.IOException; +import java.io.InputStream; + +import ezvcard.property.Email; +import ezvcard.property.Telephone; +import lombok.Cleanup; +import android.content.res.AssetManager; +import android.test.InstrumentationTestCase; +import at.bitfire.davdroid.resource.Contact; +import at.bitfire.davdroid.resource.InvalidResourceException; + +public class ContactTest extends InstrumentationTestCase { + AssetManager assetMgr; + + public void setUp() throws IOException, InvalidResourceException { + assetMgr = getInstrumentation().getContext().getResources().getAssets(); + } + + public void testReferenceVCard() throws IOException, InvalidResourceException { + Contact c = parseVCF("reference.vcf"); + assertEquals("Gump", c.getFamilyName()); + assertEquals("Forrest", c.getGivenName()); + assertEquals("Forrest Gump", c.getDisplayName()); + assertEquals("Bubba Gump Shrimp Co.", c.getOrganization().getValues().get(0)); + assertEquals("Shrimp Man", c.getJobTitle()); + + Telephone phone1 = c.getPhoneNumbers().get(0); + assertEquals("(111) 555-1212", phone1.getText()); + assertEquals("WORK", phone1.getParameters("TYPE").get(0)); + assertEquals("VOICE", phone1.getParameters("TYPE").get(1)); + + Telephone phone2 = c.getPhoneNumbers().get(1); + assertEquals("(404) 555-1212", phone2.getText()); + assertEquals("HOME", phone2.getParameters("TYPE").get(0)); + assertEquals("VOICE", phone2.getParameters("TYPE").get(1)); + + Email email = c.getEmails().get(0); + assertEquals("forrestgump@example.com", email.getValue()); + assertEquals("PREF", email.getParameters("TYPE").get(0)); + assertEquals("INTERNET", email.getParameters("TYPE").get(1)); + } + + public void testParseInvalidUnknownProperties() throws IOException, InvalidResourceException { + Contact c = parseVCF("invalid-unknown-properties.vcf"); + assertEquals("VCard with invalid unknown properties", c.getDisplayName()); + assertNull(c.getUnknownProperties()); + } + + + protected Contact parseVCF(String fname) throws IOException, InvalidResourceException { + @Cleanup InputStream in = assetMgr.open(fname, AssetManager.ACCESS_STREAMING); + Contact c = new Contact(fname, null); + c.parseEntity(in); + return c; + } +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/resource/test/EventTest.java b/app/src/androidTest/java/at/bitfire/davdroid/resource/test/EventTest.java new file mode 100644 index 00000000..89afdc1c --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/resource/test/EventTest.java @@ -0,0 +1,127 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.resource.test; + +import java.io.IOException; +import java.io.InputStream; + +import lombok.Cleanup; +import net.fortuna.ical4j.data.ParserException; +import android.content.res.AssetManager; +import android.test.InstrumentationTestCase; +import android.text.format.Time; +import at.bitfire.davdroid.resource.Event; +import at.bitfire.davdroid.resource.InvalidResourceException; + +public class EventTest extends InstrumentationTestCase { + AssetManager assetMgr; + + Event eViennaEvolution, + eOnThatDay, eAllDay1Day, eAllDay10Days, eAllDay0Sec; + + public void setUp() throws IOException, InvalidResourceException { + assetMgr = getInstrumentation().getContext().getResources().getAssets(); + + eViennaEvolution = parseCalendar("vienna-evolution.ics"); + eOnThatDay = parseCalendar("event-on-that-day.ics"); + eAllDay1Day = parseCalendar("all-day-1day.ics"); + eAllDay10Days = parseCalendar("all-day-10days.ics"); + eAllDay0Sec = parseCalendar("all-day-0sec.ics"); + + //assertEquals("Test-Ereignis im schönen Wien", e.getSummary()); + } + + + public void testStartEndTimes() throws IOException, ParserException { + // event with start+end date-time + assertEquals(1381330800000L, eViennaEvolution.getDtStartInMillis()); + assertEquals("Europe/Vienna", eViennaEvolution.getDtStartTzID()); + assertEquals(1381334400000L, eViennaEvolution.getDtEndInMillis()); + assertEquals("Europe/Vienna", eViennaEvolution.getDtEndTzID()); + } + + public void testStartEndTimesAllDay() throws IOException, ParserException { + // event with start date only + assertEquals(868838400000L, eOnThatDay.getDtStartInMillis()); + assertEquals(Time.TIMEZONE_UTC, eOnThatDay.getDtStartTzID()); + // DTEND missing in VEVENT, must have been set to DTSTART+1 day + assertEquals(868838400000L + 86400000, eOnThatDay.getDtEndInMillis()); + assertEquals(Time.TIMEZONE_UTC, eOnThatDay.getDtEndTzID()); + + // event with start+end date for all-day event (one day) + assertEquals(868838400000L, eAllDay1Day.getDtStartInMillis()); + assertEquals(Time.TIMEZONE_UTC, eAllDay1Day.getDtStartTzID()); + assertEquals(868838400000L + 86400000, eAllDay1Day.getDtEndInMillis()); + assertEquals(Time.TIMEZONE_UTC, eAllDay1Day.getDtEndTzID()); + + // event with start+end date for all-day event (ten days) + assertEquals(868838400000L, eAllDay10Days.getDtStartInMillis()); + assertEquals(Time.TIMEZONE_UTC, eAllDay10Days.getDtStartTzID()); + assertEquals(868838400000L + 10*86400000, eAllDay10Days.getDtEndInMillis()); + assertEquals(Time.TIMEZONE_UTC, eAllDay10Days.getDtEndTzID()); + + // event with start+end date on some day (invalid 0 sec-event) + assertEquals(868838400000L, eAllDay0Sec.getDtStartInMillis()); + assertEquals(Time.TIMEZONE_UTC, eAllDay0Sec.getDtStartTzID()); + // DTEND invalid in VEVENT, must have been set to DTSTART+1 day + assertEquals(868838400000L + 86400000, eAllDay0Sec.getDtEndInMillis()); + assertEquals(Time.TIMEZONE_UTC, eAllDay0Sec.getDtEndTzID()); + } + + public void testTimezoneDefToTzId() { + // test valid definition + final String VTIMEZONE_SAMPLE = // taken from RFC 4791, 5.2.2. CALDAV:calendar-timezone Property + "BEGIN:VCALENDAR\n" + + "PRODID:-//Example Corp.//CalDAV Client//EN\n" + + "VERSION:2.0\n" + + "BEGIN:VTIMEZONE\n" + + "TZID:US-Eastern\n" + + "LAST-MODIFIED:19870101T000000Z\n" + + "BEGIN:STANDARD\n" + + "DTSTART:19671029T020000\n" + + "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\n" + + "TZOFFSETFROM:-0400\n" + + "TZOFFSETTO:-0500\n" + + "TZNAME:Eastern Standard Time (US & Canada)\n" + + "END:STANDARD\n" + + "BEGIN:DAYLIGHT\n" + + "DTSTART:19870405T020000\n" + + "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4\n" + + "TZOFFSETFROM:-0500\n" + + "TZOFFSETTO:-0400\n" + + "TZNAME:Eastern Daylight Time (US & Canada)\n" + + "END:DAYLIGHT\n" + + "END:VTIMEZONE\n" + + "END:VCALENDAR"; + assertEquals("US-Eastern", Event.TimezoneDefToTzId(VTIMEZONE_SAMPLE)); + + // test null value + try { + Event.TimezoneDefToTzId(null); + fail(); + } catch(IllegalArgumentException e) { + assert(true); + } + + // test invalid time zone + try { + Event.TimezoneDefToTzId("/* invalid content */"); + fail(); + } catch(IllegalArgumentException e) { + assert(true); + } + } + + + protected Event parseCalendar(String fname) throws IOException, InvalidResourceException { + @Cleanup InputStream in = assetMgr.open(fname, AssetManager.ACCESS_STREAMING); + Event e = new Event(fname, null); + e.parseEntity(in); + return e; + } +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/resource/test/LocalCalendarTest.java b/app/src/androidTest/java/at/bitfire/davdroid/resource/test/LocalCalendarTest.java new file mode 100644 index 00000000..6fe5afea --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/resource/test/LocalCalendarTest.java @@ -0,0 +1,154 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.resource.test; + +import java.util.Calendar; + +import lombok.Cleanup; +import android.accounts.Account; +import android.annotation.TargetApi; +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.RemoteException; +import android.provider.CalendarContract; +import android.provider.CalendarContract.Attendees; +import android.provider.CalendarContract.Calendars; +import android.provider.CalendarContract.Events; +import android.provider.CalendarContract.Reminders; +import android.test.InstrumentationTestCase; +import android.util.Log; +import at.bitfire.davdroid.resource.LocalCalendar; +import at.bitfire.davdroid.resource.LocalStorageException; + +public class LocalCalendarTest extends InstrumentationTestCase { + + private static final String + TAG = "davroid.LocalCalendarTest", + calendarName = "DAVdroid_Test"; + + ContentProviderClient providerClient; + Account testAccount = new Account(calendarName, CalendarContract.ACCOUNT_TYPE_LOCAL); + LocalCalendar testCalendar; + + + // helpers + + private Uri syncAdapterURI(Uri uri) { + return uri.buildUpon() + .appendQueryParameter(Calendars.ACCOUNT_NAME, calendarName) + .appendQueryParameter(Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL) + .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true"). + build(); + } + + private long insertNewEvent() throws LocalStorageException, RemoteException { + ContentValues values = new ContentValues(); + values.put(Events.CALENDAR_ID, testCalendar.getId()); + values.put(Events.TITLE, "Test Event"); + values.put(Events.ALL_DAY, 0); + values.put(Events.DTSTART, Calendar.getInstance().getTimeInMillis()); + values.put(Events.DTEND, Calendar.getInstance().getTimeInMillis()); + values.put(Events.EVENT_TIMEZONE, "UTC"); + values.put(Events.DIRTY, 1); + return ContentUris.parseId(providerClient.insert(syncAdapterURI(Events.CONTENT_URI), values)); + } + + private void deleteEvent(long id) throws RemoteException { + providerClient.delete(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)), null, null); + } + + + // initialization + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) + protected void setUp() throws Exception { + ContentResolver resolver = getInstrumentation().getContext().getContentResolver(); + providerClient = resolver.acquireContentProviderClient(CalendarContract.AUTHORITY); + + long id; + + @Cleanup Cursor cursor = providerClient.query(Calendars.CONTENT_URI, + new String[] { Calendars._ID }, + Calendars.ACCOUNT_TYPE + "=? AND " + Calendars.NAME + "=?", + new String[] { CalendarContract.ACCOUNT_TYPE_LOCAL, calendarName }, + null); + if (cursor.moveToNext()) { + // found local test calendar + id = cursor.getLong(0); + Log.d(TAG, "Found test calendar with ID " + id); + + } else { + // no local test calendar found, create + ContentValues values = new ContentValues(); + values.put(Calendars.ACCOUNT_NAME, testAccount.name); + values.put(Calendars.ACCOUNT_TYPE, testAccount.type); + values.put(Calendars.NAME, calendarName); + values.put(Calendars.CALENDAR_DISPLAY_NAME, calendarName); + values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER); + values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT); + values.put(Calendars.SYNC_EVENTS, 0); + values.put(Calendars.VISIBLE, 1); + + if (android.os.Build.VERSION.SDK_INT >= 15) { + values.put(Calendars.ALLOWED_AVAILABILITY, Events.AVAILABILITY_BUSY + "," + Events.AVAILABILITY_FREE + "," + Events.AVAILABILITY_TENTATIVE); + values.put(Calendars.ALLOWED_ATTENDEE_TYPES, Attendees.TYPE_NONE + "," + Attendees.TYPE_OPTIONAL + "," + Attendees.TYPE_REQUIRED + "," + Attendees.TYPE_RESOURCE); + } + + Uri calendarURI = providerClient.insert(syncAdapterURI(Calendars.CONTENT_URI), values); + + id = ContentUris.parseId(calendarURI); + Log.d(TAG, "Created test calendar with ID " + id); + } + + testCalendar = new LocalCalendar(testAccount, providerClient, id, null); + } + + protected void tearDown() throws Exception { + Uri uri = ContentUris.withAppendedId(syncAdapterURI(Calendars.CONTENT_URI), testCalendar.getId()); + providerClient.delete(uri, null, null); + } + + + // tests + + public void testCTags() throws LocalStorageException { + assertNull(testCalendar.getCTag()); + + final String cTag = "just-modified"; + testCalendar.setCTag(cTag); + + assertEquals(cTag, testCalendar.getCTag()); + } + + public void testFindNew() throws LocalStorageException, RemoteException { + // at the beginning, there are no dirty events + assertTrue(testCalendar.findNew().length == 0); + assertTrue(testCalendar.findUpdated().length == 0); + + // insert a "new" event + long id = insertNewEvent(); + try { + // there must be one "new" event now + assertTrue(testCalendar.findNew().length == 1); + assertTrue(testCalendar.findUpdated().length == 0); + + // nothing has changed, the record must still be "new" + // see issue #233 + assertTrue(testCalendar.findNew().length == 1); + assertTrue(testCalendar.findUpdated().length == 0); + } finally { + deleteEvent(id); + } + } + +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/syncadapter/DavResourceFinderTest.java b/app/src/androidTest/java/at/bitfire/davdroid/syncadapter/DavResourceFinderTest.java new file mode 100644 index 00000000..bffd9080 --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/syncadapter/DavResourceFinderTest.java @@ -0,0 +1,69 @@ +package at.bitfire.davdroid.syncadapter; + +import java.io.IOException; +import java.net.URI; +import java.util.List; + +import android.test.InstrumentationTestCase; +import at.bitfire.davdroid.resource.DavResourceFinder; +import at.bitfire.davdroid.resource.ServerInfo; +import at.bitfire.davdroid.resource.ServerInfo.ResourceInfo; +import at.bitfire.davdroid.test.Constants; +import ezvcard.VCardVersion; + +public class DavResourceFinderTest extends InstrumentationTestCase { + + DavResourceFinder finder; + + @Override + protected void setUp() { + finder = new DavResourceFinder(getInstrumentation().getContext()); + } + + @Override + protected void tearDown() throws IOException { + finder.close(); + } + + + public void testFindResourcesRobohydra() throws Exception { + ServerInfo info = new ServerInfo(new URI(Constants.ROBOHYDRA_BASE), "test", "test", true); + finder.findResources(info); + + // CardDAV + assertTrue(info.isCardDAV()); + List collections = info.getAddressBooks(); + assertEquals(1, collections.size()); + + assertEquals("Default Address Book", collections.get(0).getDescription()); + assertEquals(VCardVersion.V4_0, collections.get(0).getVCardVersion()); + + // CalDAV + assertTrue(info.isCalDAV()); + collections = info.getCalendars(); + assertEquals(2, collections.size()); + + ResourceInfo resource = collections.get(0); + assertEquals("Private Calendar", resource.getTitle()); + assertEquals("This is my private calendar.", resource.getDescription()); + assertFalse(resource.isReadOnly()); + + resource = collections.get(1); + assertEquals("Work Calendar", resource.getTitle()); + assertTrue(resource.isReadOnly()); + } + + + public void testGetInitialContextURL() throws Exception { + // without SRV records, but with well-known paths + ServerInfo roboHydra = new ServerInfo(new URI(Constants.ROBOHYDRA_BASE), "test", "test", true); + assertEquals(Constants.roboHydra.resolve("/"), finder.getInitialContextURL(roboHydra, "caldav")); + assertEquals(Constants.roboHydra.resolve("/"), finder.getInitialContextURL(roboHydra, "carddav")); + + // with SRV records and well-known paths + ServerInfo iCloud = new ServerInfo(new URI("mailto:test@icloud.com"), "", "", true); + assertEquals(new URI("https://contacts.icloud.com/"), finder.getInitialContextURL(iCloud, "carddav")); + assertEquals(new URI("https://caldav.icloud.com/"), finder.getInitialContextURL(iCloud, "caldav")); + } + +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/test/ArrayUtilsTest.java b/app/src/androidTest/java/at/bitfire/davdroid/test/ArrayUtilsTest.java new file mode 100644 index 00000000..2761d2be --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/test/ArrayUtilsTest.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.test; +import java.util.Arrays; + +import junit.framework.TestCase; +import at.bitfire.davdroid.ArrayUtils; + + +public class ArrayUtilsTest extends TestCase { + + public void testPartition() { + // n == 0 + assertTrue(Arrays.deepEquals( + new Long[0][0], + ArrayUtils.partition(new Long[] { }, 5))); + + // n < max + assertTrue(Arrays.deepEquals( + new Long[][] { { 1l, 2l } }, + ArrayUtils.partition(new Long[] { 1l, 2l }, 5))); + + // n == max + assertTrue(Arrays.deepEquals( + new Long[][] { { 1l, 2l }, { 3l, 4l } }, + ArrayUtils.partition(new Long[] { 1l, 2l, 3l, 4l }, 2))); + + // n > max + assertTrue(Arrays.deepEquals( + new Long[][] { { 1l, 2l, 3l, 4l, 5l }, { 6l, 7l, 8l, 9l, 10l }, { 11l } }, + ArrayUtils.partition(new Long[] { 1l, 2l, 3l, 4l, 5l, 6l, 7l, 8l, 9l, 10l, 11l }, 5))); + } + +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/test/Constants.java b/app/src/androidTest/java/at/bitfire/davdroid/test/Constants.java new file mode 100644 index 00000000..e730c9e7 --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/test/Constants.java @@ -0,0 +1,19 @@ +package at.bitfire.davdroid.test; + +import java.net.URI; +import java.net.URISyntaxException; + +import android.util.Log; + +public class Constants { + public static final String ROBOHYDRA_BASE = "http://10.0.0.11:3000/"; + + public static URI roboHydra; + static { + try { + roboHydra = new URI(ROBOHYDRA_BASE); + } catch(URISyntaxException e) { + Log.wtf("davdroid.test.Constants", "Invalid RoboHydra base URL"); + } + } +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/test/ContactTest.java b/app/src/androidTest/java/at/bitfire/davdroid/test/ContactTest.java new file mode 100644 index 00000000..a98ed78f --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/test/ContactTest.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.test; + +import java.io.IOException; +import java.io.InputStream; + +import lombok.Cleanup; +import net.fortuna.ical4j.data.ParserException; +import android.content.res.AssetManager; +import android.test.InstrumentationTestCase; +import at.bitfire.davdroid.resource.Contact; +import ezvcard.property.Impp; + +public class ContactTest extends InstrumentationTestCase { + AssetManager assetMgr; + + public void setUp() { + assetMgr = getInstrumentation().getContext().getResources().getAssets(); + } + + public void testIMPP() throws IOException { + Contact c = parseVCard("impp.vcf"); + assertEquals("test mctest", c.getDisplayName()); + + Impp jabber = c.getImpps().get(0); + assertNull(jabber.getProtocol()); + assertEquals("test-without-valid-scheme@test.tld", jabber.getHandle()); + } + + + public void testParseVcard3() throws IOException, ParserException { + Contact c = parseVCard("vcard3-sample1.vcf"); + + assertEquals("Forrest Gump", c.getDisplayName()); + assertEquals("Forrest", c.getGivenName()); + assertEquals("Gump", c.getFamilyName()); + + assertEquals(2, c.getPhoneNumbers().size()); + assertEquals("(111) 555-1212", c.getPhoneNumbers().get(0).getText()); + + assertEquals(1, c.getEmails().size()); + assertEquals("forrestgump@example.com", c.getEmails().get(0).getValue()); + + assertFalse(c.isStarred()); + } + + + private Contact parseVCard(String fileName) throws IOException { + @Cleanup InputStream in = assetMgr.open(fileName, AssetManager.ACCESS_STREAMING); + + Contact c = new Contact(fileName, null); + c.parseEntity(in); + + return c; + } +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/test/URLUtilsTest.java b/app/src/androidTest/java/at/bitfire/davdroid/test/URLUtilsTest.java new file mode 100644 index 00000000..7f6a8513 --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/test/URLUtilsTest.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.test; + +import java.net.URI; + +import junit.framework.TestCase; +import at.bitfire.davdroid.URIUtils; + + +public class URLUtilsTest extends TestCase { + + /* RFC 1738 p17 HTTP URLs: + hpath = hsegment *[ "/" hsegment ] + hsegment = *[ uchar | ";" | ":" | "@" | "&" | "=" ] + uchar = unreserved | escape + unreserved = alpha | digit | safe | extra + alpha = lowalpha | hialpha + lowalpha = ... + hialpha = ... + digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | + "8" | "9" + safe = "$" | "-" | "_" | "." | "+" + extra = "!" | "*" | "'" | "(" | ")" | "," + escape = "%" hex hex + */ + + + public void testEnsureTrailingSlash() throws Exception { + assertEquals("/test/", URIUtils.ensureTrailingSlash("/test")); + assertEquals("/test/", URIUtils.ensureTrailingSlash("/test/")); + + String withoutSlash = "http://www.test.at/dav/collection", + withSlash = withoutSlash + "/"; + assertEquals(new URI(withSlash), URIUtils.ensureTrailingSlash(new URI(withoutSlash))); + assertEquals(new URI(withSlash), URIUtils.ensureTrailingSlash(new URI(withSlash))); + } + + public void testParseURI() throws Exception { + // don't escape valid characters + String validPath = "/;:@&=$-_.+!*'(),"; + assertEquals(new URI("https://www.test.at:123" + validPath), URIUtils.parseURI("https://www.test.at:123" + validPath)); + assertEquals(new URI(validPath), URIUtils.parseURI(validPath)); + + // keep literal IPv6 addresses (only in host name) + assertEquals(new URI("https://[1:2::1]/"), URIUtils.parseURI("https://[1:2::1]/")); + + // ~ as home directory + assertEquals(new URI("http://www.test.at/~user1/"), URIUtils.parseURI("http://www.test.at/~user1/")); + assertEquals(new URI("http://www.test.at/~user1/"), URIUtils.parseURI("http://www.test.at/%7euser1/")); + + // @ in directory name + assertEquals(new URI("http://www.test.at/user@server.com/"), URIUtils.parseURI("http://www.test.at/user@server.com/")); + assertEquals(new URI("http://www.test.at/user@server.com/"), URIUtils.parseURI("http://www.test.at/user%40server.com/")); + } +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/webdav/DavRedirectStrategyTest.java b/app/src/androidTest/java/at/bitfire/davdroid/webdav/DavRedirectStrategyTest.java new file mode 100644 index 00000000..8b92629d --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/webdav/DavRedirectStrategyTest.java @@ -0,0 +1,73 @@ +package at.bitfire.davdroid.webdav; + +import java.io.IOException; + +import junit.framework.TestCase; +import at.bitfire.davdroid.test.Constants; +import ch.boye.httpclientandroidlib.HttpResponse; +import ch.boye.httpclientandroidlib.client.methods.HttpOptions; +import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest; +import ch.boye.httpclientandroidlib.client.protocol.HttpClientContext; +import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient; +import ch.boye.httpclientandroidlib.impl.client.HttpClientBuilder; +import ch.boye.httpclientandroidlib.protocol.HttpContext; + +public class DavRedirectStrategyTest extends TestCase { + + CloseableHttpClient httpClient; + DavRedirectStrategy strategy = DavRedirectStrategy.INSTANCE; + + @Override + protected void setUp() { + httpClient = HttpClientBuilder.create() + .useSystemProperties() + .disableRedirectHandling() + .build(); + } + + @Override + protected void tearDown() throws IOException { + httpClient.close(); + } + + + // happy cases + + public void testNonRedirection() throws Exception { + HttpUriRequest request = new HttpOptions(Constants.roboHydra); + HttpResponse response = httpClient.execute(request); + assertFalse(strategy.isRedirected(request, response, null)); + } + + public void testDefaultRedirection() throws Exception { + final String newLocation = "/new-location"; + + HttpContext context = HttpClientContext.create(); + HttpUriRequest request = new HttpOptions(Constants.roboHydra.resolve("redirect/301?to=" + newLocation)); + HttpResponse response = httpClient.execute(request, context); + assertTrue(strategy.isRedirected(request, response, context)); + + HttpUriRequest redirected = strategy.getRedirect(request, response, context); + assertEquals(Constants.roboHydra.resolve(newLocation), redirected.getURI()); + } + + + // error cases + + public void testMissingLocation() throws Exception { + HttpContext context = HttpClientContext.create(); + HttpUriRequest request = new HttpOptions(Constants.roboHydra.resolve("redirect/without-location")); + HttpResponse response = httpClient.execute(request, context); + assertFalse(strategy.isRedirected(request, response, context)); + } + + public void testRelativeLocation() throws Exception { + HttpContext context = HttpClientContext.create(); + HttpUriRequest request = new HttpOptions(Constants.roboHydra.resolve("redirect/relative")); + HttpResponse response = httpClient.execute(request, context); + assertTrue(strategy.isRedirected(request, response, context)); + + HttpUriRequest redirected = strategy.getRedirect(request, response, context); + assertEquals(Constants.roboHydra.resolve("/new/location"), redirected.getURI()); + } +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/webdav/TlsSniSocketFactoryTest.java b/app/src/androidTest/java/at/bitfire/davdroid/webdav/TlsSniSocketFactoryTest.java new file mode 100644 index 00000000..f00844d5 --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/webdav/TlsSniSocketFactoryTest.java @@ -0,0 +1,27 @@ +package at.bitfire.davdroid.webdav; + +import java.io.IOException; + +import javax.net.ssl.SSLSocket; + +import android.util.Log; +import junit.framework.TestCase; + +public class TlsSniSocketFactoryTest extends TestCase { + private static final String TAG = "davdroid.TlsSniSocketFactoryTest"; + + public void testCiphers() throws IOException { + SSLSocket socket = (SSLSocket)TlsSniSocketFactory.INSTANCE.createSocket(null); + + Log.i(TAG, "Enabled:"); + for (String cipher : socket.getEnabledCipherSuites()) + Log.i(TAG, cipher); + + Log.i(TAG, "Supported:"); + for (String cipher : socket.getSupportedCipherSuites()) + Log.i(TAG, cipher); + + assert(true); + } + +} diff --git a/app/src/androidTest/java/at/bitfire/davdroid/webdav/WebDavResourceTest.java b/app/src/androidTest/java/at/bitfire/davdroid/webdav/WebDavResourceTest.java new file mode 100644 index 00000000..9110093d --- /dev/null +++ b/app/src/androidTest/java/at/bitfire/davdroid/webdav/WebDavResourceTest.java @@ -0,0 +1,233 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import java.io.InputStream; +import java.net.URI; +import java.util.Arrays; +import java.util.List; + +import javax.net.ssl.SSLPeerUnverifiedException; + +import lombok.Cleanup; + +import org.apache.commons.io.IOUtils; + +import android.content.res.AssetManager; +import android.test.InstrumentationTestCase; +import at.bitfire.davdroid.test.Constants; +import at.bitfire.davdroid.webdav.HttpPropfind.Mode; +import at.bitfire.davdroid.webdav.WebDavResource.PutMode; +import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient; + +// tests require running robohydra! + +public class WebDavResourceTest extends InstrumentationTestCase { + static byte[] SAMPLE_CONTENT = new byte[] { 1, 2, 3, 4, 5 }; + + final static String PATH_SIMPLE_FILE = "collection/new.file"; + + AssetManager assetMgr; + CloseableHttpClient httpClient; + + WebDavResource baseDAV; + WebDavResource simpleFile, + davCollection, davNonExistingFile, davExistingFile, + davInvalid; + + @Override + protected void setUp() throws Exception { + httpClient = DavHttpClient.create(true, true); + + assetMgr = getInstrumentation().getContext().getResources().getAssets(); + + baseDAV = new WebDavResource(httpClient, Constants.roboHydra.resolve("/dav/")); + + simpleFile = new WebDavResource(httpClient, Constants.roboHydra.resolve("assets/test.random")); + + davCollection = new WebDavResource(httpClient, Constants.roboHydra.resolve("dav/")); + davNonExistingFile = new WebDavResource(davCollection, "collection/new.file"); + davExistingFile = new WebDavResource(davCollection, "collection/existing.file"); + + davInvalid = new WebDavResource(httpClient, Constants.roboHydra.resolve("dav-invalid/")); + } + + @Override + protected void tearDown() throws Exception { + httpClient.close(); + } + + + /* test feature detection */ + + public void testOptions() throws Exception { + String[] davMethods = new String[] { "PROPFIND", "GET", "PUT", "DELETE", "REPORT" }, + davCapabilities = new String[] { "addressbook", "calendar-access" }; + + WebDavResource capable = new WebDavResource(baseDAV); + capable.options(); + for (String davMethod : davMethods) + assertTrue(capable.supportsMethod(davMethod)); + for (String capability : davCapabilities) + assertTrue(capable.supportsDAV(capability)); + } + + public void testPropfindCurrentUserPrincipal() throws Exception { + davCollection.propfind(HttpPropfind.Mode.CURRENT_USER_PRINCIPAL); + assertEquals("/dav/principals/users/test", davCollection.getCurrentUserPrincipal()); + + try { + simpleFile.propfind(HttpPropfind.Mode.CURRENT_USER_PRINCIPAL); + fail(); + + } catch(DavException ex) { + } + assertNull(simpleFile.getCurrentUserPrincipal()); + } + + public void testPropfindHomeSets() throws Exception { + WebDavResource dav = new WebDavResource(davCollection, "principals/users/test"); + dav.propfind(HttpPropfind.Mode.HOME_SETS); + assertEquals("/dav/addressbooks/test/", dav.getAddressbookHomeSet()); + assertEquals("/dav/calendars/test/", dav.getCalendarHomeSet()); + } + + public void testPropfindAddressBooks() throws Exception { + WebDavResource dav = new WebDavResource(davCollection, "addressbooks/test"); + dav.propfind(HttpPropfind.Mode.CARDDAV_COLLECTIONS); + assertEquals(2, dav.getMembers().size()); + for (WebDavResource member : dav.getMembers()) { + if (member.getName().equals("default-v4.vcf")) + assertTrue(member.isAddressBook()); + else + assertFalse(member.isAddressBook()); + assertFalse(member.isCalendar()); + } + } + + public void testPropfindCalendars() throws Exception { + WebDavResource dav = new WebDavResource(davCollection, "calendars/test"); + dav.propfind(Mode.CALDAV_COLLECTIONS); + assertEquals(3, dav.getMembers().size()); + assertEquals("0xFF00FF", dav.getMembers().get(2).getColor()); + for (WebDavResource member : dav.getMembers()) { + if (member.getName().contains(".ics")) + assertTrue(member.isCalendar()); + else + assertFalse(member.isCalendar()); + assertFalse(member.isAddressBook()); + } + } + + public void testPropfindTrailingSlashes() throws Exception { + final String principalOK = "/principals/ok"; + + String requestPaths[] = { + "/dav/collection-response-with-trailing-slash", + "/dav/collection-response-with-trailing-slash/", + "/dav/collection-response-without-trailing-slash", + "/dav/collection-response-without-trailing-slash/" + }; + + for (String path : requestPaths) { + WebDavResource davSlash = new WebDavResource(davCollection, path); + davSlash.propfind(Mode.CARDDAV_COLLECTIONS); + assertEquals(principalOK, davSlash.getCurrentUserPrincipal()); + } + } + + + /* test normal HTTP/WebDAV */ + + public void testPropfindRedirection() throws Exception { + // PROPFIND redirection + WebDavResource redirected = new WebDavResource(baseDAV, "/redirect/301?to=/dav/"); + redirected.propfind(Mode.CURRENT_USER_PRINCIPAL); + assertEquals("/dav/", redirected.getLocation().getPath()); + } + + public void testGet() throws Exception { + simpleFile.get("*/*"); + @Cleanup InputStream is = assetMgr.open("test.random", AssetManager.ACCESS_STREAMING); + byte[] expected = IOUtils.toByteArray(is); + assertTrue(Arrays.equals(expected, simpleFile.getContent())); + } + + public void testGetHttpsWithSni() throws Exception { + WebDavResource file = new WebDavResource(httpClient, new URI("https://sni.velox.ch")); + + boolean sniWorking = false; + try { + file.get("*/*"); + sniWorking = true; + } catch (SSLPeerUnverifiedException e) { + } + + assertTrue(sniWorking); + } + + public void testMultiGet() throws Exception { + WebDavResource davAddressBook = new WebDavResource(davCollection, "addressbooks/default.vcf"); + davAddressBook.multiGet(DavMultiget.Type.ADDRESS_BOOK, new String[] { "1.vcf", "2.vcf" }); + assertEquals("My Book", davAddressBook.getDisplayName()); + assertEquals(2, davAddressBook.getMembers().size()); + for (WebDavResource member : davAddressBook.getMembers()) + assertNotNull(member.getContent()); + } + + public void testPutAddDontOverwrite() throws Exception { + // should succeed on a non-existing file + assertEquals("has-just-been-created", davNonExistingFile.put(SAMPLE_CONTENT, PutMode.ADD_DONT_OVERWRITE)); + + // should fail on an existing file + try { + davExistingFile.put(SAMPLE_CONTENT, PutMode.ADD_DONT_OVERWRITE); + fail(); + } catch(PreconditionFailedException ex) { + } + } + + public void testPutUpdateDontOverwrite() throws Exception { + // should succeed on an existing file + assertEquals("has-just-been-updated", davExistingFile.put(SAMPLE_CONTENT, PutMode.UPDATE_DONT_OVERWRITE)); + + // should fail on a non-existing file + try { + davNonExistingFile.put(SAMPLE_CONTENT, PutMode.UPDATE_DONT_OVERWRITE); + fail(); + } catch(PreconditionFailedException ex) { + } + } + + public void testDelete() throws Exception { + // should succeed on an existing file + davExistingFile.delete(); + + // should fail on a non-existing file + try { + davNonExistingFile.delete(); + fail(); + } catch (NotFoundException e) { + } + } + + + /* test CalDAV/CardDAV */ + + + /* special test */ + + public void testInvalidURLs() throws Exception { + WebDavResource dav = new WebDavResource(davInvalid, "addressbooks/%7euser1/"); + dav.propfind(HttpPropfind.Mode.CARDDAV_COLLECTIONS); + List members = dav.getMembers(); + assertEquals(1, members.size()); + assertEquals(Constants.ROBOHYDRA_BASE + "dav-invalid/addressbooks/~user1/My%20Contacts:1.vcf/", members.get(0).getLocation().toASCIIString()); + } + +} diff --git a/app/src/androidTest/res/drawable-hdpi/ic_launcher.png b/app/src/androidTest/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..96a442e5b8e9394ccf50bab9988cb2316026245d GIT binary patch literal 9397 zcmV;mBud+fP)L`9r|n3#ts(U@pVoQ)(ZPc(6i z8k}N`MvWQ78F(rhG(?6FnFXYo>28{yZ}%O}TvdDT_5P?j=iW=V`8=UNc_}`JbG!ST zs@lK(TWkH+P**sB$A`cEY%Y53cQ}1&6`x-M$Cz&{o9bLU^M-%^mY?+vedlvt$RT-^ zu|w7}IaWaljBq#|I%Mpo!Wc2bbZF3KF9|D%wZe{YFM=hJAv$>j>nhx`=Wis#KG!cJA5x!4)f) zezMz1?Vn$GnZNjbFXH(pK83nn!^3=+^*kTTs5rV9Dq^XS(IKO!mKt5!dSmb3IVCxZ z8TTk5IE)F1V29$G7v#j9d-hy&_pdg8?kT4)zqr>?`}I%W>(?GO%*C&}?Fp|bI*~2&KZ$%^B6R&1~2kA{`CWy+>F-x=z-f{_&vyu_3yp{jtw(*syi% zu3t2|4{c~LJXRt2m>rMg2V_kLltCZ<`m>qcI?BPP?6hf``|e!rZEFszeYQ3f-*nAS zZ+h1$mFwy+7156lkB(k6)!1fUbJCxgIBK38$jj5cC$r&YXN)nr#PY=tJaLc?C_o?j+8H3Q>891JJ9&$l-r+-SG#q)*;r52% z@nlKflb65o%s*Jt)!pw1k{vIoQIvoJ0Y&Msiw0X!qJ)_47G*?aJ6bJFLh_4b$5&1k5wN>du*>6#i7R9T8; z7>EHOV=ue7mo77SJPwER4(A+s?n0JjYK)b}Om6n>ke?0JR=jTI+RFBg_iwb7k%n*2 zR_M0DJ9x+0zxba4(B1y^JQ_Nj6dlP5PGXvSq8fF#mxrFYj3d9(V#jJwt+IqU9+8+D z6C6Us1OI$d8OF!3+Hm1 zW5in zXV^%U35HooOpSmeqlG6e0kUMYNonKp1vr|My9}4-WO+uOxe_c-o&}%voNYHkqtle% z5yQ_^oozSUUNu30EQSAl!Q%(%3G1NXENSMjCL*Vx-Td2~rk(}d z8pT!HZe>1r5EGuz`pgsg@^yQEi=BIa#meLq0!?{TZ}q#}=7UC9_l=w|wv+pP!g4#! zRys6EN$Jv}#U47$k&)pDzvks}LGfPku6P9p!56Py)~1)W(11n7n}`Wx!=;_JTiu#d zpCqx=hEk@t4sp?!j{W}wP@V-=Pd=T^>6IKBy;#mLA7hCe{V7B3@I7Ipa}L`MbF|YQ z)$BNWsiEnoNHrtJli|n8cOnn4NyF=8MbVxgof0>Uv%wM_j94a;8(LMjlL~E(99gJ*2%JtNtAkD@j;^ za~Y~&j6uY{=Rv5S4joH*RW_m9N{ZSN0HhAwFyJNok zS9kx$>wMf%tUi&Eb`6u0lWJ|k?A-42(lp2UmS(PrAc(24wexRiHUieMwf$o%m6$xs zp#-SdBUu2D5`v;(9-sm&kN2M74c&AvKe_v@tQ|dzJ2qSgQHpnUP(iQ?J%Il;Jdyp# z7}cpq6Kdm+FS~zS4Eo;fuO=DFP*UlpO|_CNt5&NUqBvQWxmg7#ARvMf=%#H@p%RZ` zjK$hMbNb+vVP3UlkfIt&ptJ<00Ic{Ka+lF+&w;OEs1O2#V8~O|R*Gq9TIgM&UqM&bZOXBwnbC? zDr))NR&g>lwVgcmnx`K1$)PTTw3m}-T11^ZkY{}jQ@lGD$XzJIcVFkYBBW=o_}TUU zt@yd{Jz;@~72x#!RG(#ira6}v-*J#<{@@^OI-Q2T^}=IKLubsa&V-%WwlF1s7fz~u zMdQTV7SnRet#^`VO0V7H(?59X{uy+S`(sorO@2-+qioUdo9+6r4#|jb=?t50oh42R z{}I>Krut|YKkOc|O|M>y#(3YA;I(i+MiHSfwbJA$jIUr$Y2i|u)*>@2eUYk`j4C5r z>61dKu!AqM_E7#DoDzbd-bfT%AYXUUB{SS|{b{`5^?wz1{PVQgTlvyqOX8(#GTz(U zNPhnj>$lC`xaD56`TjW&uW8p~qikP*F8kHFM0frzdk%UNGjb1O$%uLK`0-)2UsZ3L z#+j+CI_8k4VslL%$aVR@joX>M-@odbX!os$xY$HDIOCokY?{Q0v2kQErf|ZlN>D9w zC+2}E&?rDdi#%))$p%P4C_xGXu=@U~_<|V4L|{>TP$XBp$5pCPXLzK3!;gP>7=QNi zkNOur`>xY=@VSpB#LsN9JKpOz({ANcdv>?K+D_*_HZ<;9>kplj^Ph5!e&&a#?(3vK z_Q@}D_M5kGcx^AuaI~qKYUnb1Mj-n;MURXa)+x7~e2gbMW|gw?5Rg zTOMlo>6zIJ$VNVgn(@kTSL0eP)nR35IHpoHM2W#h6cNmTm@-9`dFJ$;k(S`7Lg@RY zp!hNmb9un!O4Wt05ANDGirv(B14gW| zwjP}C9bK{J`qZ_S2o)b`RonR-b8~y8)$H0`+gg6>#^wu8eCp9xA9B>>8(KRizI?+^ zAJ#i>*({qM-c4gBB~5dzg(wj!HA`hkh!aDl5>u&J;>2K#Ax2)2wt|L!9X;(=*jy!`r4_FhCBoRxNjXNv(~jGQ|%<}%K6RimaBJcP0v}oCgRN3B;oiM)opj? zXm;;tv3q-yy}NqMOr^~3&1lW$w3}UK_IT2sCrkYx5$&6e2A%g;QZUX~A&L!2rFd0p z5%men@^zN_Xw2|v%*c2|wQfkN4r6u&k;LxYY+w3{KY#cie)!iz>(yAgt=&-+Sy2V& z9BJxI+VMKQ%dvY~x>gmEijj3ss_*NAT(8d1@DQ6e&#Ln&6Qk>wHrh>;V2nvomC`8& z(w?`?*_^3u-TJrMzv2~7dH(XLJvUOXk4U8oW6Ol)YsawhIB{GdvIzu1hzMTrE)cvB z%2GxMpaF89<9uF(?cfN(BNR?wwWvCZ6e62+G_{$+;`yjgLj{(^z*zzwd;K3RElb*%=??P zm+lLY0@Y}^kVdMYX5M)YJ~8h=i(S{q#NfU0xPTao4WPDQL=Y_;vg=p%iay1_`<0Ga zMG&<(pOU+bI2u9_g8IJBTqGX*3@G$Zc`pj0f@)vd2?Aj`ms>DHg>;w~p}HXV(*VJX zphd;fht9qL3E)D8h$$A;SGl22Ygv>`iU=A)z=1ZYN$|2`*$`R)?KD>$tw_e9h_x~eX_udS~Q%yz?48i*aIa+_wx|j{B zsG7mwZ)6M3dmvgMC3K-66;ML(9o2xU!F8+qF)>v{1;ip)6v_I)6law|rd_Dx2oV|n z(Qm_PUnTTuKFG)w%s|)lS!w~Lm$k|Al=0djocyHU;>1H=!N}0E0lSV^b2^6~^lUco zyoH+|_!li3#euHd4TJS8=CLaHG9H8g&h3Xm z#>BkpUBAmae(#)qO3)ZMG3irM=5IzA^s+)w86=tIMT{&?Awux<(k2>U#n`c&@Z?u= z%=#BoO-9Nc^?)hz*YW~~tU8rLR-MZBJsY_7fp2r~mY>q-O;L%5Fp?}V6CK=F(18U3 znxB8ZR0TT{)T64RDt!+yFgp!JXGP0|It0Hz2Em#YfRv>O>8A?J=Sz!nq<|{&mW=?~ zDQT{S6PH0|jwy37t+0Ob6izz)JdRlNEUbyk>-K?}FOT=Dj9SuS_0nTFd+A^D?Bo83 zTkicXcW=IuZoZd(Dl;&#`LI;_s?e;OH9quf?*XuV0O$Qh0j~HWKpA|PXV4&b2zs z@W5<)dtovIRZ@gvsi$^s;v05(XwF3$lJ;wzYfE`46fnT7>!qt|hWHRE>yQP)i8= zVbC|O{Ud6%kwGcch>>|pE-=?cW;TDR0lE5Nw7l66lr-zIYT3bj^ujCn$b0{ZO;gwK z#}}W(*T3~in$6ZCpbB98pftPTo;!K>U;H*7_}t4m;;4i9#^2t`pS<=jsnx198);d3 z-M6Mx{7-c0A-jhJQ`5mBy8TBnfbr2~sER5E5oz}=so34cg)GYarRWi8w#W$%G{?Z*4xDb#LX1B1 zg!4G{m~*)H_J8J^SNt`XU-fxjea`>p_$Qyn*Dn18*WdPCp8oWw^XU)%kfRQHMgfQh z1j_ua@O4G%QK;&YH3Y9(q!hkgOUCkcVH5N0Ug(EPX%H6qCfPqg))qrd#ec^47dBu- z=sRkmjGS>3K(tfRTo;zCXO-74hV;y1!vCN}v|w?AWR$YpYXs@Dr?iNLKD9s|2)0aHY!TKTYhwMI z7b#54h!H6rUU9+xnL$g6h?t?Li5guXPY1g)$bI$~rHWP%QkYJ6Y-U^0C(@*$ruN2*zn0QRBOeVpgMFbT%k!Dn1*u#%J^y)enX1K;0~ z%3Q zP(b%}P!Loj6M{v96(Qa~K!bq-V-P89U_K)0zHC_F#L==3IPh2hHG6&?rxvQ%|EljR zfGIDyu=rIrl1dyjuMfwuh?pXZmARwNZ?GbW;5BH5D#nN|WbGm+UGAh7_AcG>4&|{0 zrg?k@h8zm!0A|5Zo%X%g|2tBPKHHB6`~4h?I@bepDe6?^f8w zBnzfOf|j{kR5m6BLRr0$!RZ$PHSk*)tyjkws*DpyHIiiL*8o(Smx(OKT7@D&Y3OI^ zEUMtKa2*SLjt(eJsZsLsrgV`A+xL(~JN#JU6+L)gCe%VuSNbCzTr09w>eZ#779SKV z)m)@#TNVy|q3Tz_U`^7MY`l}`GU~OlQi|*cprX?tm@tIV+8kOGkaa=9Y<{N|RZ)ns zHlgnz2S%qwK9wXjest~Ux$YNNA{0?6Xpv{_mqYt8D`g&7Yb~>lX+HP&AK<=+Zl_kO z6a2g`^4=9W92GQ3e9Mk6?DlzlkIM`iOzwk*5L81TcuyYkI-<3^@49_+^XC7&N}SL1 zh$kIBxb`9+v}acfV?FQ zN#04eHe0*j{pz=zOj3#EHLrT3e)O;3xqpCWrl$e)PcD9jQ4P-8_zyZg^M7i|*kOuj znsvlwNUsy5+01^P_sqMOjXjxKwHn4)$87t-MWZZ*5Dbit4|D9vL+spsJ0JPd?{Ms) zFW^<@yqjZ=IvG%$ck_Cu9|b8CvoV%5P5IZWzs>i4`~`N+-p`7a6RbLHJ;nxtSB#Mb z`1I552=9DrYWFNZ{-=Mt;SVo5@3cmv`IZT@@>#~zCe-=qENxsn+uHfL`e?SbT3IQ_ zt~e)Lcirs_S5^X#?hDYmgV%8QQDe+?>*1&0e^BnaeZz(&D~3<)#QuUL8h*NlXgtr| z&a{_Z)o9FK_U5<0!E3N|yY1P2g%J9s*?!zF78+NSb%!ix)tbQ09oO&|U$~Bwk35^- zec9VN^xz{043e^xD}WEmzh8d^-~Pd8**bEfd+I?HuO~n4SksoN8LRPUy={E<@BjRMUh?X71Xaey>t^$&Eq2B7)u_r$ z|IQwpG52G!F$J5fRo1LqLB7iKz_!bI@27skX~+Eze|Y}IBuRp?hR7z|eA~7B<99#7 zrX4r2a_tCDUb_}Cg)g!OEVeJ5AEVRyb!9~f4OL68qhZZRP0l*>MdkxvxXeGWx$T>+ zI^X!wnYQDnwK9?i)j)eLXJU2Cw>~>R?72@MecvT7;h~2gATow_cbc)$Ws+xNSB{++ zo^tTp^y*(-Y-XF=$XyoBJnMN9+p!Qrep1)%ym_v7zZH{;u~L>T=4XP!f^?uC4ULUR zdl`>x+DVkHVd;|9#N*oubBFQEyRT#UK^0c7T}l)eEEFS)qvZl%f>#I;iCwAWb=kW0 z(e#lm51o?d>D|kgtTscVQCNDAXMAjxSX&{_Qf)T((wMHWWLbz6WpPXP0(3_SBWwI19Vx?$i6WUqP$4O|wjNbYzst$z{58`cBhm z&F(N-KeXFzo#aC|6BbC($As#B8X=}ggpDyQUp|Q>9cG$47#>TQn%T(eHA`5se7KnZ zF_dj_6NN0xS-oZ%Nj%PTpK=MC zw*4IMGls_v)mokI)Dph*pD<)7prEF|j6I$2=XF=Ua3z;BN^yt&H@G%7& zWnL7*e0S9svjSP>kuc;VCbZXUN3G7D8`G@!Qnjt=p=7yC?QH0tsa@RsuPMLj@wf-c z|LV)H$Auga+MTAU#>)eeuh_L`!qC=Ls|{m}Cy)|w6#aP}w6_-ya~9LF z{dQAPa-|&ME858gIK=}lVK7MLT~Oye&UM9y?0X=8Qmvb*)=X}iv%Me)Gqav+FWdGT zuk&#ak~?2Kzf}w)xZuKGx%+`1?Ecoq?*H@EjFm%C6OT577vWKoJB z$A^sIasm!5TGOFFGmHkKNTE7KW3nveUq1bt4Uj)!1_6BJ zU6=EoPrjVdk+pQX+j-GTpQS&&^43tT43kuRlvE8fGdYc!1|m)3WCuwlqB>NeQc0** zYE&wTj*QpuPLfJ)j2$(`sI@k@oR!^9d(3&Kd6r3*<)pooPNzq=)1%#NQ;nAsF*5VR zOYXQC;B^4*Sik--jy?J`uDj-! zSep}9YT4*SOrT2I6MF4H+EZFRPh+}^b4@i8OYk9Y&86o*Y4(`Ax1W4#tX^5m6LjZPb61LF2?qBy?B_?1YE!nej)R5c8qG`2s_uF`Cu+ z`X_$#2Ur#!Pw0WVd60fYG8A#y55LDyJ!Yt$5G6Efb<6Nr%-BTC_|llMB?%*A5%rOX z`fyBbD5g@4Ns^)P;F7zjv{t6u?k1J0kR*v#Dhair3iXjH^^qz=!xd`vm`W`oN-Wj_ zNML7~t!rRbc|9I0mUjpEgOJ9XGg2;vjDZ;b~V638P!uVuejytg~ci-I(n9#M6AR=mQG0YjoLKGPgFp(jS4Pn7UJR)Et z-8ZsqWsRLXri#f_BSeWIat3P+Q3Td1#ws={2CLGpDdvrgP#KD7 z&SnaR^#_Bsq;Xt;kyI^}iX~1WYzdHamc$tH1#Mz6f<2(WuH^s%^yXK78Gyg}{;LNA zoW%$)#R!a0wv&q%qj%+~i3^k&1jY!ljfi82Vr$~W5G6u&$Wp0VqR3*bDIWLE4Y64K ze08)CmeFrq2>QGFSDAk%Rhs}$r*rJVNuoO(~AJ!PG{T~d_i(dQ;OsQc+q&twwlJV|`Bv$N}R$K=uxCPyc!RBBXfRjRcZi5yAQk|YKj*>d`|Xw~ckP!!SW%^gsH z4oDR1AJt?S?}B;<&e0TPFsNAMQwxCt69o{uA>=K^qd1+MST3tptj8GHnN(upgb*ji zq`i%b+{{=o7ByB78@8!x_Gs&uqLOKv_6{gO2b4jbc8YT@EEzqBp!v_c?XXFx9Dq zb{!I|Nu<;4kZbyl3*LDg#$f7`nKwT9p9|2|t&fmAe64Of^c3TKI%Q?_^+uxaj|?xL zw5U4G#YlpQDngbfM)q85qt=DJt|y5nG){VqE;V8I&WBCAH+|pe@QT+};^BWB8(lGB zqe!DD7GqI`0pj%h;hm z;n?F&(5YS1X4{T?Hf24&;~ic?rDC*Zgk;*ga9b~Je`?R%gBQy3U5$!cEi-#s>T+d# zWH}Mbv|6p1R<`wiiPB32Gn*u}EQxC^LGJIR?H}~g*|#s5IQY`pJzcYP=0El5RWIen z8*k;5(^qldFJ}(enhxl1pnB_vPi5uu!@1|-9|Owd=%J>WPwQ>dkLW|!5WV<$<73Xb z{0CRJT1OpP567)vYea*J7*!3_M-nC`C)l*@dKzsw^5El5v)K$c-nf?sZ)?i>Gc=yt zg{xL=urnv{!j}h=hh{KFAjIS@=h9CPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L02dMf02dMgXP?qi00007bV*G`2ipt~ z7YY(F`_Sb8017EdL_t(o!?l=uuwPYm$3JVI^ZWho?|E--5|R`W6G$K=z*GYY6wr=V zEHLAs& zdz|M!d-acV?}e00xbt)P@m$z^s#fV*k#SgXB4;4pFT(w@xz)o_l~EwJ+$tL zNA}&l{N}CqzO8^B)M@;g^aHT<;0E84yNhu{N${eJ-?VeV-AUA6q$<9trt}a{U45TFsn9Sc6zfp($j8t2s@dE zQIjAUBn)CY?J)11fS?@`1`%Nx6NL#$Z0Usk7(Wr4STgIdiMw7!!ptNtBYrmL$nY(+rzsSZg&+Q(Pts z$DVsczi`HH^ri&>wJ9FAf9p&De1OdZH!;t<6V-n!4>5RGht>sq2l{?Fa6~?LaQm$9 z9qH`6yjb)4PhAIa?cbkttcHHF=ZgDOlWSCc`VaTB=hp)doVH}{g9J0z z{OG}rx?{_LG>2kT!Sf8oqKD@j#DD_oG}lq0#F53O8AgO^qo8w6oGP^*|D}1SXUk7K zb?V*KdY9iC3G_f;Tb_CB@TqH89N00=&{%tU%c0Z4WB~ApI*tQ-I@60@=bck#y}*T6 z_R1w!Pet&si6M<0X$&@1Z04|OhSLnh!5CX8&N-6E$;g1?;NIcJ!9M@ET6asjDj{j& zq&1Y$9Lh>#7>)s?>Lr;~P$jdD%&Hf*{8+t^cGKb)1Y-;$qr{4!>WIP!krE;qzA0ie zH@2QMam0}lG!0Rtu2d9Jhk!tC3eGyD1bu2t1_*& znD@VXDUHfZeztiTyAJ-0ENzq8EH4L{qM4F8hdRitic@fz!#TyN5{GdxF+&jQ7@$l6 zDL9*@Sw_A%6O4hL>RjG2?L1CC{!f_IyJ&pj%>v_aJj(1 zDV}G@zl}MeEcR)=MBzMj!s=}<^ zGdSzCOStu`m-76U#|fg&xSoPB<%f3P={hr%`p}{nf+USozR$hK7$G3*$9{2!b{no?XWStM8y#?82#n6GW?7)Zsa` zwL!I2XXA1vS#2G_6uFg)uUPcjE9|${UC9d@_w0xRuPYew-0*;GI=nx){rvMUu(54@ z+`1-W3}TdRyVvvF=0|BZ+svA_fYc`R9sDKlJoSV8^oiAcd+nE5_tZVqd%^b&f>BQz zGBTL-|M&8(H=O;xQ=e^A=e^iz^4+6@yKlSf%8Tv#hqkcmS4VRN-hS^#_`+wt2f#&F zoaoiN8`U^;=?_+H4ewj^5AQhK+SC`?KJ^PeVnke)?{!I}B<(sU&3He<>2?MWWu%2Z z{8ENr@N(U$qFI3=v-$PTS07#Z@0&k3QOG}i+j)HBi%%Z=`tcW^UCejx+4hFXpTF~> z6_NH`)m1V01y2Phns1H@BEv%=rBZ<`6)ly05y^ASTBkN~;?g=vr9P;=m7CX$|G)Zgm+aiXZ~uaNy+(I$oqD4|rBaJZ zrIPx7!4u>8HcdFJC#TdexmzBje$|6hQ{z`W;j zcxEL`omomE>(d+x8Qd8VhX=5+`P#GV58evMdoP*&lTI}9fl8%JsjEQ2FXPkIUzaTk zaNk#c^;wYqAW|>-DX%0C?1}#Zoic`Di%g1kcS7qn!=Ut&(rcy6c zEP5*Vl6GWL2O9olCKpP^6ib5fJT(SUCo~-tix$s^a?N*TuSl&?#P^M4X@Pb!L1}-x z&WA*#CC1=+BE_;txmKWDDTfD-_Gz_Ib&Z~KTI()QX%w`p;#2A}c%F3r-vD)*@$xL` zN{seU@}^QO)(>T_xfWpdaeovRE7^CZPMr}#|!d*|R6{H=+M{MV$Mp3LNPKT_t5 z(-+S5yz=?J*A+!U{KSTh8xFttSbqQdFU>bSjT8Q$)Ky#JnbOd}k;7ZR_W37=|NQzh jFn-Lp|K;W1YU6(Zg`N}+zmb=x00000NkvXXu0mjf_|!_9 literal 0 HcmV?d00001 diff --git a/app/src/androidTest/res/drawable-mdpi/ic_launcher.png b/app/src/androidTest/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..359047dfa4ed206e41e2354f9c6b307e713efe32 GIT binary patch literal 5237 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 literal 0 HcmV?d00001 diff --git a/app/src/androidTest/res/drawable-xhdpi/ic_launcher.png b/app/src/androidTest/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..71c6d760f05183ef8a47c614d8d13380c8528499 GIT binary patch literal 14383 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 literal 0 HcmV?d00001 diff --git a/app/src/androidTest/res/values/strings.xml b/app/src/androidTest/res/values/strings.xml new file mode 100644 index 00000000..2539643a --- /dev/null +++ b/app/src/androidTest/res/values/strings.xml @@ -0,0 +1,6 @@ + + + + DavdroidTest + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..dfa974cc --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/at/bitfire/davdroid/ArrayUtils.java b/app/src/main/java/at/bitfire/davdroid/ArrayUtils.java new file mode 100644 index 00000000..f4f050db --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/ArrayUtils.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid; + +import java.lang.reflect.Array; + +public class ArrayUtils { + + @SuppressWarnings("unchecked") + public static T[][] partition(T[] bigArray, int max) { + int nItems = bigArray.length; + int nPartArrays = (nItems + max-1)/max; + + T[][] partArrays = (T[][])Array.newInstance(bigArray.getClass().getComponentType(), nPartArrays, 0); + + // nItems is now the number of remaining items + for (int i = 0; nItems > 0; i++) { + int n = (nItems < max) ? nItems : max; + partArrays[i] = (T[])Array.newInstance(bigArray.getClass().getComponentType(), n); + System.arraycopy(bigArray, i*max, partArrays[i], 0, n); + + nItems -= n; + } + + return partArrays; + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/Constants.java b/app/src/main/java/at/bitfire/davdroid/Constants.java new file mode 100644 index 00000000..123ca3a5 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/Constants.java @@ -0,0 +1,18 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid; + +public class Constants { + public static final String + APP_VERSION = "0.6.9", + ACCOUNT_TYPE = "bitfire.at.davdroid", + WEB_URL_HELP = "https://davdroid.bitfire.at/configuration?pk_campaign=davdroid-app", + + SETTING_DISABLE_COMPRESSION = "disable_compression", + SETTING_NETWORK_LOGGING = "network_logging"; +} diff --git a/app/src/main/java/at/bitfire/davdroid/MainActivity.java b/app/src/main/java/at/bitfire/davdroid/MainActivity.java new file mode 100644 index 00000000..a665f6a0 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/MainActivity.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.provider.Settings; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.TextView; +import at.bitfire.davdroid.syncadapter.AddAccountActivity; +import at.bitfire.davdroid.syncadapter.GeneralSettingsActivity; + +public class MainActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_main); + + TextView tvWorkaround = (TextView)findViewById(R.id.text_workaround); + if (fromPlayStore()) { + tvWorkaround.setVisibility(View.VISIBLE); + tvWorkaround.setText(Html.fromHtml(getString(R.string.html_main_workaround))); + tvWorkaround.setMovementMethod(LinkMovementMethod.getInstance()); + } + + TextView tvInfo = (TextView)findViewById(R.id.text_info); + tvInfo.setText(Html.fromHtml(getString(R.string.html_main_info, Constants.APP_VERSION))); + tvInfo.setMovementMethod(LinkMovementMethod.getInstance()); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.main_activity, menu); + return true; + } + + + public void addAccount(MenuItem item) { + Intent intent = new Intent(this, AddAccountActivity.class); + startActivity(intent); + } + + public void showDebugSettings(MenuItem item) { + Intent intent = new Intent(this, GeneralSettingsActivity.class); + startActivity(intent); + } + + public void showSyncSettings(MenuItem item) { + Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS); + startActivity(intent); + } + + public void showWebsite(MenuItem item) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(Constants.WEB_URL_HELP + "&pk_kwd=main-activity")); + startActivity(intent); + } + + + private boolean fromPlayStore() { + try { + return "com.android.vending".equals(getPackageManager().getInstallerPackageName("at.bitfire.davdroid")); + } catch(IllegalArgumentException e) { + } + return false; + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/URIUtils.java b/app/src/main/java/at/bitfire/davdroid/URIUtils.java new file mode 100644 index 00000000..a1ab9038 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/URIUtils.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid; + +import java.net.URI; +import java.net.URISyntaxException; + +import android.util.Log; + +public class URIUtils { + private static final String TAG = "davdroid.URIUtils"; + + + public static String ensureTrailingSlash(String href) { + if (!href.endsWith("/")) { + Log.d(TAG, "Implicitly appending trailing slash to collection " + href); + return href + "/"; + } else + return href; + } + + public static URI ensureTrailingSlash(URI href) { + if (!href.getPath().endsWith("/")) { + try { + URI newURI = new URI(href.getScheme(), href.getAuthority(), href.getPath() + "/", null, null); + Log.d(TAG, "Appended trailing slash to collection " + href + " -> " + newURI); + href = newURI; + } catch (URISyntaxException e) { + } + } + return href; + } + + + /** + * Parse a received absolute/relative URL and generate a normalized URI that can be compared. + * @param original URI to be parsed, may be absolute or relative + * @return normalized URI + * @throws URISyntaxException + */ + public static URI parseURI(String original) throws URISyntaxException { + URI raw = URI.create(original); + URI uri = new URI(raw.getScheme(), raw.getAuthority(), raw.getPath(), raw.getQuery(), raw.getFragment()); + Log.v(TAG, "Normalized URL " + original + " -> " + uri.toASCIIString()); + return uri; + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/CalDavCalendar.java b/app/src/main/java/at/bitfire/davdroid/resource/CalDavCalendar.java new file mode 100644 index 00000000..47a595a9 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/CalDavCalendar.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.resource; + +import java.net.URISyntaxException; + +import at.bitfire.davdroid.webdav.DavMultiget; +import org.apache.http.impl.client.CloseableHttpClient; + +public class CalDavCalendar extends RemoteCollection { + //private final static String TAG = "davdroid.CalDavCalendar"; + + @Override + protected String memberContentType() { + return "text/calendar"; + } + + @Override + protected DavMultiget.Type multiGetType() { + return DavMultiget.Type.CALENDAR; + } + + @Override + protected Event newResourceSkeleton(String name, String ETag) { + return new Event(name, ETag); + } + + + public CalDavCalendar(CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException { + super(httpClient, baseURL, user, password, preemptiveAuth); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/CardDavAddressBook.java b/app/src/main/java/at/bitfire/davdroid/resource/CardDavAddressBook.java new file mode 100644 index 00000000..98b9b825 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/CardDavAddressBook.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.resource; + +import java.net.URISyntaxException; + +import at.bitfire.davdroid.webdav.DavMultiget; +import org.apache.http.impl.client.CloseableHttpClient; + +public class CardDavAddressBook extends RemoteCollection { + //private final static String TAG = "davdroid.CardDavAddressBook"; + + @Override + protected String memberContentType() { + return Contact.MIME_TYPE; + } + + @Override + protected DavMultiget.Type multiGetType() { + return DavMultiget.Type.ADDRESS_BOOK; + } + + @Override + protected Contact newResourceSkeleton(String name, String ETag) { + return new Contact(name, ETag); + } + + + public CardDavAddressBook(CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException { + super(httpClient, baseURL, user, password, preemptiveAuth); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/Contact.java b/app/src/main/java/at/bitfire/davdroid/resource/Contact.java new file mode 100644 index 00000000..d301450c --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/Contact.java @@ -0,0 +1,410 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.resource; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import org.apache.commons.lang.StringUtils; + +import android.util.Log; +import at.bitfire.davdroid.Constants; +import ezvcard.Ezvcard; +import ezvcard.VCard; +import ezvcard.VCardVersion; +import ezvcard.ValidationWarnings; +import ezvcard.parameter.EmailType; +import ezvcard.parameter.ImageType; +import ezvcard.parameter.TelephoneType; +import ezvcard.property.Address; +import ezvcard.property.Anniversary; +import ezvcard.property.Birthday; +import ezvcard.property.Categories; +import ezvcard.property.Email; +import ezvcard.property.FormattedName; +import ezvcard.property.Impp; +import ezvcard.property.Logo; +import ezvcard.property.Nickname; +import ezvcard.property.Note; +import ezvcard.property.Organization; +import ezvcard.property.Photo; +import ezvcard.property.RawProperty; +import ezvcard.property.Revision; +import ezvcard.property.Role; +import ezvcard.property.Sound; +import ezvcard.property.Source; +import ezvcard.property.StructuredName; +import ezvcard.property.Telephone; +import ezvcard.property.Title; +import ezvcard.property.Uid; +import ezvcard.property.Url; + + +/** + * Represents a contact. Locally, this is a Contact in the Android + * device; remote, this is a VCard. + */ +@ToString(callSuper = true) +public class Contact extends Resource { + private final static String TAG = "davdroid.Contact"; + + public final static String MIME_TYPE = "text/vcard"; + + public final static String + PROPERTY_STARRED = "X-DAVDROID-STARRED", + PROPERTY_PHONETIC_FIRST_NAME = "X-PHONETIC-FIRST-NAME", + PROPERTY_PHONETIC_MIDDLE_NAME = "X-PHONETIC-MIDDLE-NAME", + PROPERTY_PHONETIC_LAST_NAME = "X-PHONETIC-LAST-NAME", + PROPERTY_SIP = "X-SIP"; + + public final static EmailType EMAIL_TYPE_MOBILE = EmailType.get("X-MOBILE"); + + public final static TelephoneType + PHONE_TYPE_CALLBACK = TelephoneType.get("X-CALLBACK"), + PHONE_TYPE_COMPANY_MAIN = TelephoneType.get("X-COMPANY_MAIN"), + PHONE_TYPE_RADIO = TelephoneType.get("X-RADIO"), + PHONE_TYPE_ASSISTANT = TelephoneType.get("X-ASSISTANT"), + PHONE_TYPE_MMS = TelephoneType.get("X-MMS"); + + @Getter @Setter private String unknownProperties; + + @Getter @Setter private boolean starred; + + @Getter @Setter private String displayName, nickName; + @Getter @Setter private String prefix, givenName, middleName, familyName, suffix; + @Getter @Setter private String phoneticGivenName, phoneticMiddleName, phoneticFamilyName; + @Getter @Setter private String note; + @Getter @Setter private Organization organization; + @Getter @Setter private String jobTitle, jobDescription; + + @Getter @Setter private byte[] photo; + + @Getter @Setter private Anniversary anniversary; + @Getter @Setter private Birthday birthDay; + + @Getter private List phoneNumbers = new LinkedList(); + @Getter private List emails = new LinkedList(); + @Getter private List impps = new LinkedList(); + @Getter private List
addresses = new LinkedList
(); + @Getter private List categories = new LinkedList(); + @Getter private List URLs = new LinkedList(); + + + /* instance methods */ + + public Contact(String name, String ETag) { + super(name, ETag); + } + + public Contact(long localID, String resourceName, String eTag) { + super(localID, resourceName, eTag); + } + + + @Override + public void initialize() { + generateUID(); + name = uid + ".vcf"; + } + + protected void generateUID() { + uid = UUID.randomUUID().toString(); + } + + + /* VCard methods */ + + @Override + public void parseEntity(InputStream is) throws IOException { + VCard vcard = Ezvcard.parse(is).first(); + if (vcard == null) + return; + + // now work through all supported properties + // supported properties are removed from the VCard after parsing + // so that only unknown properties are left and can be stored separately + + // UID + Uid uid = vcard.getUid(); + if (uid != null) { + this.uid = uid.getValue(); + vcard.removeProperties(Uid.class); + } else { + Log.w(TAG, "Received VCard without UID, generating new one"); + generateUID(); + } + + // X-DAVDROID-STARRED + RawProperty starred = vcard.getExtendedProperty(PROPERTY_STARRED); + if (starred != null && starred.getValue() != null) { + this.starred = starred.getValue().equals("1"); + vcard.removeExtendedProperty(PROPERTY_STARRED); + } else + this.starred = false; + + // FN + FormattedName fn = vcard.getFormattedName(); + if (fn != null) { + displayName = fn.getValue(); + vcard.removeProperties(FormattedName.class); + } else + Log.w(TAG, "Received invalid VCard without FN (formatted name) property"); + + // N + StructuredName n = vcard.getStructuredName(); + if (n != null) { + prefix = StringUtils.join(n.getPrefixes(), " "); + givenName = n.getGiven(); + middleName = StringUtils.join(n.getAdditional(), " "); + familyName = n.getFamily(); + suffix = StringUtils.join(n.getSuffixes(), " "); + vcard.removeProperties(StructuredName.class); + } + + // phonetic names + RawProperty + phoneticFirstName = vcard.getExtendedProperty(PROPERTY_PHONETIC_FIRST_NAME), + phoneticMiddleName = vcard.getExtendedProperty(PROPERTY_PHONETIC_MIDDLE_NAME), + phoneticLastName = vcard.getExtendedProperty(PROPERTY_PHONETIC_LAST_NAME); + if (phoneticFirstName != null) { + phoneticGivenName = phoneticFirstName.getValue(); + vcard.removeExtendedProperty(PROPERTY_PHONETIC_FIRST_NAME); + } + if (phoneticMiddleName != null) { + this.phoneticMiddleName = phoneticMiddleName.getValue(); + vcard.removeExtendedProperty(PROPERTY_PHONETIC_MIDDLE_NAME); + } + if (phoneticLastName != null) { + phoneticFamilyName = phoneticLastName.getValue(); + vcard.removeExtendedProperty(PROPERTY_PHONETIC_LAST_NAME); + } + + // TEL + phoneNumbers = vcard.getTelephoneNumbers(); + vcard.removeProperties(Telephone.class); + + // EMAIL + emails = vcard.getEmails(); + vcard.removeProperties(Email.class); + + // PHOTO + for (Photo photo : vcard.getPhotos()) { + this.photo = photo.getData(); + vcard.removeProperties(Photo.class); + break; + } + + // ORG + organization = vcard.getOrganization(); + vcard.removeProperties(Organization.class); + // TITLE + for (Title title : vcard.getTitles()) { + jobTitle = title.getValue(); + vcard.removeProperties(Title.class); + break; + } + // ROLE + for (Role role : vcard.getRoles()) { + this.jobDescription = role.getValue(); + vcard.removeProperties(Role.class); + break; + } + + // IMPP + impps = vcard.getImpps(); + vcard.removeProperties(Impp.class); + + // NICKNAME + Nickname nicknames = vcard.getNickname(); + if (nicknames != null) { + if (nicknames.getValues() != null) + nickName = StringUtils.join(nicknames.getValues(), ", "); + vcard.removeProperties(Nickname.class); + } + + // NOTE + List notes = new LinkedList(); + for (Note note : vcard.getNotes()) + notes.add(note.getValue()); + if (!notes.isEmpty()) + note = StringUtils.join(notes, "\n---\n"); + vcard.removeProperties(Note.class); + + // ADR + addresses = vcard.getAddresses(); + vcard.removeProperties(Address.class); + + // CATEGORY + Categories categories = vcard.getCategories(); + if (categories != null) + this.categories = categories.getValues(); + vcard.removeProperties(Categories.class); + + // URL + for (Url url : vcard.getUrls()) + URLs.add(url.getValue()); + vcard.removeProperties(Url.class); + + // BDAY + birthDay = vcard.getBirthday(); + vcard.removeProperties(Birthday.class); + // ANNIVERSARY + anniversary = vcard.getAnniversary(); + vcard.removeProperties(Anniversary.class); + + // X-SIP + for (RawProperty sip : vcard.getExtendedProperties(PROPERTY_SIP)) + impps.add(new Impp("sip", sip.getValue())); + vcard.removeExtendedProperty(PROPERTY_SIP); + + // remove binary properties because of potential OutOfMemory / TransactionTooLarge exceptions + vcard.removeProperties(Logo.class); + vcard.removeProperties(Sound.class); + // remove properties that don't apply anymore + vcard.removeProperties(Revision.class); + vcard.removeProperties(Source.class); + // store all remaining properties into unknownProperties + if (!vcard.getProperties().isEmpty() || !vcard.getExtendedProperties().isEmpty()) + try { + unknownProperties = vcard.write(); + } catch(Exception e) { + Log.w(TAG, "Couldn't store unknown properties (maybe illegal syntax), dropping them"); + } + } + + + @Override + public ByteArrayOutputStream toEntity() throws IOException { + VCard vcard = null; + try { + if (unknownProperties != null) + vcard = Ezvcard.parse(unknownProperties).first(); + } catch (Exception e) { + Log.w(TAG, "Couldn't parse original property set, beginning from scratch"); + } + if (vcard == null) + vcard = new VCard(); + + if (uid != null) + vcard.setUid(new Uid(uid)); + else + Log.wtf(TAG, "Generating VCard without UID"); + + if (starred) + vcard.setExtendedProperty(PROPERTY_STARRED, "1"); + + if (displayName != null) + vcard.setFormattedName(displayName); + else if (organization != null && organization.getValues() != null && organization.getValues().get(0) != null) + vcard.setFormattedName(organization.getValues().get(0)); + else + Log.w(TAG, "No FN (formatted name) available to generate VCard"); + + // N + if (familyName != null || middleName != null || givenName != null) { + StructuredName n = new StructuredName(); + if (prefix != null) + for (String p : StringUtils.split(prefix)) + n.addPrefix(p); + n.setGiven(givenName); + if (middleName != null) + for (String middle : StringUtils.split(middleName)) + n.addAdditional(middle); + n.setFamily(familyName); + if (suffix != null) + for (String s : StringUtils.split(suffix)) + n.addSuffix(s); + vcard.setStructuredName(n); + } + + // phonetic names + if (phoneticGivenName != null) + vcard.addExtendedProperty(PROPERTY_PHONETIC_FIRST_NAME, phoneticGivenName); + if (phoneticMiddleName != null) + vcard.addExtendedProperty(PROPERTY_PHONETIC_MIDDLE_NAME, phoneticMiddleName); + if (phoneticFamilyName != null) + vcard.addExtendedProperty(PROPERTY_PHONETIC_LAST_NAME, phoneticFamilyName); + + // TEL + for (Telephone phoneNumber : phoneNumbers) + vcard.addTelephoneNumber(phoneNumber); + + // EMAIL + for (Email email : emails) + vcard.addEmail(email); + + // ORG, TITLE, ROLE + if (organization != null) + vcard.setOrganization(organization); + if (jobTitle != null) + vcard.addTitle(jobTitle); + if (jobDescription != null) + vcard.addRole(jobDescription); + + // IMPP + for (Impp impp : impps) + vcard.addImpp(impp); + + // NICKNAME + if (!StringUtils.isBlank(nickName)) + vcard.setNickname(nickName); + + // NOTE + if (!StringUtils.isBlank(note)) + vcard.addNote(note); + + // ADR + for (Address address : addresses) + vcard.addAddress(address); + + // CATEGORY + if (!categories.isEmpty()) + vcard.setCategories(categories.toArray(new String[0])); + + // URL + for (String url : URLs) + vcard.addUrl(url); + + // ANNIVERSARY + if (anniversary != null) + vcard.setAnniversary(anniversary); + // BDAY + if (birthDay != null) + vcard.setBirthday(birthDay); + + // PHOTO + if (photo != null) + vcard.addPhoto(new Photo(photo, ImageType.JPEG)); + + // PRODID, REV + vcard.setProductId("DAVdroid/" + Constants.APP_VERSION + " (ez-vcard/" + Ezvcard.VERSION + ")"); + vcard.setRevision(Revision.now()); + + // validate and print warnings + ValidationWarnings warnings = vcard.validate(VCardVersion.V3_0); + if (!warnings.isEmpty()) + Log.w(TAG, "Created potentially invalid VCard! " + warnings); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + Ezvcard + .write(vcard) + .version(VCardVersion.V3_0) + .versionStrict(false) + .prodId(false) // we provide our own PRODID + .go(os); + return os; + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java b/app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java new file mode 100644 index 00000000..745e9b09 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/DavResourceFinder.java @@ -0,0 +1,298 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.resource; + +import java.io.Closeable; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.LinkedList; +import java.util.List; + +import org.xbill.DNS.Lookup; +import org.xbill.DNS.Record; +import org.xbill.DNS.SRVRecord; +import org.xbill.DNS.TXTRecord; +import org.xbill.DNS.TextParseException; +import org.xbill.DNS.Type; + +import android.content.Context; +import android.util.Log; +import at.bitfire.davdroid.R; +import at.bitfire.davdroid.webdav.DavException; +import at.bitfire.davdroid.webdav.DavHttpClient; +import at.bitfire.davdroid.webdav.DavIncapableException; +import at.bitfire.davdroid.webdav.HttpPropfind.Mode; +import at.bitfire.davdroid.webdav.NotAuthorizedException; +import at.bitfire.davdroid.webdav.WebDavResource; +import org.apache.http.HttpException; +import org.apache.http.impl.client.CloseableHttpClient; +import ezvcard.VCardVersion; + +public class DavResourceFinder implements Closeable { + private final static String TAG = "davdroid.DavResourceFinder"; + + protected Context context; + protected CloseableHttpClient httpClient; + + + public DavResourceFinder(Context context) { + this.context = context; + + // disable compression and enable network logging for debugging purposes + httpClient = DavHttpClient.create(true, true); + } + + @Override + public void close() throws IOException { + httpClient.close(); + } + + + public void findResources(ServerInfo serverInfo) throws URISyntaxException, DavException, HttpException, IOException { + // CardDAV + WebDavResource principal = getCurrentUserPrincipal(serverInfo, "carddav"); + if (principal != null) { + serverInfo.setCardDAV(true); + + principal.propfind(Mode.HOME_SETS); + String pathAddressBooks = principal.getAddressbookHomeSet(); + if (pathAddressBooks != null) { + Log.i(TAG, "Found address book home set: " + pathAddressBooks); + + WebDavResource homeSetAddressBooks = new WebDavResource(principal, pathAddressBooks); + if (checkHomesetCapabilities(homeSetAddressBooks, "addressbook")) { + homeSetAddressBooks.propfind(Mode.CARDDAV_COLLECTIONS); + + List addressBooks = new LinkedList(); + if (homeSetAddressBooks.getMembers() != null) + for (WebDavResource resource : homeSetAddressBooks.getMembers()) + if (resource.isAddressBook()) { + Log.i(TAG, "Found address book: " + resource.getLocation().getPath()); + ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo( + ServerInfo.ResourceInfo.Type.ADDRESS_BOOK, + resource.isReadOnly(), + resource.getLocation().toString(), + resource.getDisplayName(), + resource.getDescription(), resource.getColor() + ); + + VCardVersion version = resource.getVCardVersion(); + if (version == null) + version = VCardVersion.V3_0; // VCard 3.0 MUST be supported + info.setVCardVersion(version); + + addressBooks.add(info); + } + serverInfo.setAddressBooks(addressBooks); + } else + Log.w(TAG, "Found address-book home set, but it doesn't advertise CardDAV support"); + } + } + + // CalDAV + principal = getCurrentUserPrincipal(serverInfo, "caldav"); + if (principal != null) { + serverInfo.setCalDAV(true); + + principal.propfind(Mode.HOME_SETS); + String pathCalendars = principal.getCalendarHomeSet(); + if (pathCalendars != null) { + Log.i(TAG, "Found calendar home set: " + pathCalendars); + + WebDavResource homeSetCalendars = new WebDavResource(principal, pathCalendars); + if (checkHomesetCapabilities(homeSetCalendars, "calendar-access")) { + homeSetCalendars.propfind(Mode.CALDAV_COLLECTIONS); + + List calendars = new LinkedList(); + if (homeSetCalendars.getMembers() != null) + for (WebDavResource resource : homeSetCalendars.getMembers()) + if (resource.isCalendar()) { + Log.i(TAG, "Found calendar: " + resource.getLocation().getPath()); + if (resource.getSupportedComponents() != null) { + // CALDAV:supported-calendar-component-set available + boolean supportsEvents = false; + for (String supportedComponent : resource.getSupportedComponents()) + if (supportedComponent.equalsIgnoreCase("VEVENT")) + supportsEvents = true; + if (!supportsEvents) { // ignore collections without VEVENT support + Log.i(TAG, "Ignoring this calendar because of missing VEVENT support"); + continue; + } + } + ServerInfo.ResourceInfo info = new ServerInfo.ResourceInfo( + ServerInfo.ResourceInfo.Type.CALENDAR, + resource.isReadOnly(), + resource.getLocation().toString(), + resource.getDisplayName(), + resource.getDescription(), resource.getColor() + ); + info.setTimezone(resource.getTimezone()); + calendars.add(info); + } + serverInfo.setCalendars(calendars); + } else + Log.w(TAG, "Found calendar home set, but it doesn't advertise CalDAV support"); + } + } + + if (!serverInfo.isCalDAV() && !serverInfo.isCardDAV()) + throw new DavIncapableException(context.getString(R.string.setup_neither_caldav_nor_carddav)); + + } + + + /** + * Finds the initial service URL from a given base URI (HTTP[S] or mailto URI, user name, password) + * @param serverInfo User-given service information (including base URI, i.e. HTTP[S] URL+user name+password or mailto URI and password) + * @param serviceName Service name ("carddav" or "caldav") + * @return Initial service URL (HTTP/HTTPS), without user credentials + * @throws URISyntaxException when the user-given URI is invalid + * @throws MalformedURLException when the user-given URI is invalid + */ + public URI getInitialContextURL(ServerInfo serverInfo, String serviceName) throws URISyntaxException, MalformedURLException { + String scheme = null, + domain = null; + int port = -1; + String path = "/"; + + URI baseURI = serverInfo.getBaseURI(); + if ("mailto".equalsIgnoreCase(baseURI.getScheme())) { + // mailto URIs + String mailbox = serverInfo.getBaseURI().getSchemeSpecificPart(); + + // determine service FQDN + int pos = mailbox.lastIndexOf("@"); + if (pos == -1) + throw new URISyntaxException(mailbox, "Missing @ sign"); + + scheme = "https"; + domain = mailbox.substring(pos + 1); + if (domain.isEmpty()) + throw new URISyntaxException(mailbox, "Missing domain name"); + } else { + // HTTP(S) URLs + scheme = baseURI.getScheme(); + domain = baseURI.getHost(); + port = baseURI.getPort(); + path = baseURI.getPath(); + } + + // try to determine FQDN and port number using SRV records + try { + String name = "_" + serviceName + "s._tcp." + domain; + Log.d(TAG, "Looking up SRV records for " + name); + Record[] records = new Lookup(name, Type.SRV).run(); + if (records != null && records.length >= 1) { + SRVRecord srv = selectSRVRecord(records); + + scheme = "https"; + domain = srv.getTarget().toString(true); + port = srv.getPort(); + Log.d(TAG, "Found " + serviceName + "s service for " + domain + " -> " + domain + ":" + port); + + if (port == 443) // no reason to explicitly give the default port + port = -1; + + // SRV record found, look for TXT record too (for initial context path) + records = new Lookup(name, Type.TXT).run(); + if (records != null && records.length >= 1) { + TXTRecord txt = (TXTRecord)records[0]; + for (Object o : txt.getStrings().toArray()) { + String segment = (String)o; + if (segment.startsWith("path=")) { + path = segment.substring(5); + Log.d(TAG, "Found initial context path for " + serviceName + " at " + domain + " -> " + path); + break; + } + } + } + } + } catch (TextParseException e) { + throw new URISyntaxException(domain, "Invalid domain name"); + } + + return new URI(scheme, null, domain, port, path, null, null); + } + + + /** + * Detects the current-user-principal for a given WebDavResource. At first, /.well-known/ is tried. Only + * if no current-user-principal can be detected for the .well-known location, the given location of the resource + * is tried. + * @param resource Location that will be queried + * @param serviceName Well-known service name ("carddav", "caldav") + * @return WebDavResource of current-user-principal for the given service, or null if it can't be found + * + * TODO: If a TXT record is given, always use it instead of trying .well-known first + */ + WebDavResource getCurrentUserPrincipal(ServerInfo serverInfo, String serviceName) throws URISyntaxException, IOException, NotAuthorizedException { + URI initialURL = getInitialContextURL(serverInfo, serviceName); + if (initialURL != null) { + // determine base URL (host name and initial context path) + WebDavResource base = new WebDavResource(httpClient, + initialURL, + serverInfo.getUserName(), serverInfo.getPassword(), serverInfo.isAuthPreemptive()); + + // look for well-known service (RFC 5785) + try { + WebDavResource wellKnown = new WebDavResource(base, "/.well-known/" + serviceName); + wellKnown.propfind(Mode.CURRENT_USER_PRINCIPAL); + if (wellKnown.getCurrentUserPrincipal() != null) + return new WebDavResource(wellKnown, wellKnown.getCurrentUserPrincipal()); + } catch (NotAuthorizedException e) { + Log.w(TAG, "Not authorized for well-known " + serviceName + " service detection", e); + throw e; + } catch (URISyntaxException e) { + Log.w(TAG, "Well-known" + serviceName + " service detection failed because of invalid URIs", e); + } catch (HttpException e) { + Log.d(TAG, "Well-known " + serviceName + " service detection failed with HTTP error", e); + } catch (DavException e) { + Log.w(TAG, "Well-known " + serviceName + " service detection failed with unexpected DAV response", e); + } + + // fall back to user-given initial context path + try { + base.propfind(Mode.CURRENT_USER_PRINCIPAL); + if (base.getCurrentUserPrincipal() != null) + return new WebDavResource(base, base.getCurrentUserPrincipal()); + } catch (NotAuthorizedException e) { + Log.e(TAG, "Not authorized for querying principal", e); + throw e; + } catch (HttpException e) { + Log.e(TAG, "HTTP error when querying principal", e); + } catch (DavException e) { + Log.e(TAG, "DAV error when querying principal", e); + } + Log.i(TAG, "Couldn't find current-user-principal for service " + serviceName); + } + return null; + } + + public static boolean checkHomesetCapabilities(WebDavResource resource, String davCapability) throws URISyntaxException, IOException { + // check for necessary capabilities + try { + resource.options(); + if (resource.supportsDAV(davCapability) && + resource.supportsMethod("PROPFIND")) // check only for methods that MUST be available for home sets + return true; + } catch(HttpException e) { + // for instance, 405 Method not allowed + } + return false; + } + + + SRVRecord selectSRVRecord(Record[] records) { + if (records.length > 1) + Log.w(TAG, "Multiple SRV records not supported yet; using first one"); + return (SRVRecord)records[0]; + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/Event.java b/app/src/main/java/at/bitfire/davdroid/resource/Event.java new file mode 100644 index 00000000..91f68012 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/Event.java @@ -0,0 +1,368 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.resource; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.util.Calendar; +import java.util.LinkedList; +import java.util.List; +import java.util.SimpleTimeZone; +import java.util.TimeZone; + +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import net.fortuna.ical4j.data.CalendarBuilder; +import net.fortuna.ical4j.data.CalendarOutputter; +import net.fortuna.ical4j.data.ParserException; +import net.fortuna.ical4j.model.Component; +import net.fortuna.ical4j.model.ComponentList; +import net.fortuna.ical4j.model.Date; +import net.fortuna.ical4j.model.DateTime; +import net.fortuna.ical4j.model.DefaultTimeZoneRegistryFactory; +import net.fortuna.ical4j.model.Property; +import net.fortuna.ical4j.model.PropertyList; +import net.fortuna.ical4j.model.TimeZoneRegistry; +import net.fortuna.ical4j.model.ValidationException; +import net.fortuna.ical4j.model.component.VAlarm; +import net.fortuna.ical4j.model.component.VEvent; +import net.fortuna.ical4j.model.component.VTimeZone; +import net.fortuna.ical4j.model.parameter.Value; +import net.fortuna.ical4j.model.property.Attendee; +import net.fortuna.ical4j.model.property.Clazz; +import net.fortuna.ical4j.model.property.DateProperty; +import net.fortuna.ical4j.model.property.Description; +import net.fortuna.ical4j.model.property.DtEnd; +import net.fortuna.ical4j.model.property.DtStart; +import net.fortuna.ical4j.model.property.Duration; +import net.fortuna.ical4j.model.property.ExDate; +import net.fortuna.ical4j.model.property.ExRule; +import net.fortuna.ical4j.model.property.LastModified; +import net.fortuna.ical4j.model.property.Location; +import net.fortuna.ical4j.model.property.Organizer; +import net.fortuna.ical4j.model.property.ProdId; +import net.fortuna.ical4j.model.property.RDate; +import net.fortuna.ical4j.model.property.RRule; +import net.fortuna.ical4j.model.property.Status; +import net.fortuna.ical4j.model.property.Summary; +import net.fortuna.ical4j.model.property.Transp; +import net.fortuna.ical4j.model.property.Uid; +import net.fortuna.ical4j.model.property.Version; +import net.fortuna.ical4j.util.SimpleHostInfo; +import net.fortuna.ical4j.util.UidGenerator; +import android.text.format.Time; +import android.util.Log; +import at.bitfire.davdroid.Constants; +import at.bitfire.davdroid.syncadapter.DavSyncAdapter; + + +public class Event extends Resource { + private final static String TAG = "davdroid.Event"; + + public final static String MIME_TYPE = "text/calendar"; + + private final static TimeZoneRegistry tzRegistry = new DefaultTimeZoneRegistryFactory().createRegistry(); + + @Getter @Setter private String summary, location, description; + + @Getter private DtStart dtStart; + @Getter private DtEnd dtEnd; + @Getter @Setter private Duration duration; + @Getter @Setter private RDate rdate; + @Getter @Setter private RRule rrule; + @Getter @Setter private ExDate exdate; + @Getter @Setter private ExRule exrule; + + @Getter @Setter private Boolean forPublic; + @Getter @Setter private Status status; + + @Getter @Setter private boolean opaque; + + @Getter @Setter private Organizer organizer; + @Getter private List attendees = new LinkedList(); + public void addAttendee(Attendee attendee) { + attendees.add(attendee); + } + + @Getter private List alarms = new LinkedList(); + public void addAlarm(VAlarm alarm) { + alarms.add(alarm); + } + + + public Event(String name, String ETag) { + super(name, ETag); + } + + public Event(long localID, String name, String ETag) { + super(localID, name, ETag); + } + + + @Override + public void initialize() { + generateUID(); + name = uid.replace("@", "_") + ".ics"; + } + + protected void generateUID() { + UidGenerator generator = new UidGenerator(new SimpleHostInfo(DavSyncAdapter.getAndroidID()), String.valueOf(android.os.Process.myPid())); + uid = generator.generateUid().getValue(); + } + + + @Override + @SuppressWarnings("unchecked") + public void parseEntity(@NonNull InputStream entity) throws IOException, InvalidResourceException { + net.fortuna.ical4j.model.Calendar ical; + try { + CalendarBuilder builder = new CalendarBuilder(); + ical = builder.build(entity); + + if (ical == null) + throw new InvalidResourceException("No iCalendar found"); + } catch (ParserException e) { + throw new InvalidResourceException(e); + } + + // event + ComponentList events = ical.getComponents(Component.VEVENT); + if (events == null || events.isEmpty()) + throw new InvalidResourceException("No VEVENT found"); + VEvent event = (VEvent)events.get(0); + + if (event.getUid() != null) + uid = event.getUid().getValue(); + else { + Log.w(TAG, "Received VEVENT without UID, generating new one"); + generateUID(); + } + + if ((dtStart = event.getStartDate()) == null || (dtEnd = event.getEndDate()) == null) + throw new InvalidResourceException("Invalid start time/end time/duration"); + + if (hasTime(dtStart)) { + validateTimeZone(dtStart); + validateTimeZone(dtEnd); + } + + // all-day events and "events on that day": + // * related UNIX times must be in UTC + // * must have a duration (set to one day if missing) + if (!hasTime(dtStart) && !dtEnd.getDate().after(dtStart.getDate())) { + Log.i(TAG, "Repairing iCal: DTEND := DTSTART+1"); + Calendar c = Calendar.getInstance(TimeZone.getTimeZone(Time.TIMEZONE_UTC)); + c.setTime(dtStart.getDate()); + c.add(Calendar.DATE, 1); + dtEnd.setDate(new Date(c.getTimeInMillis())); + } + + rrule = (RRule)event.getProperty(Property.RRULE); + rdate = (RDate)event.getProperty(Property.RDATE); + exrule = (ExRule)event.getProperty(Property.EXRULE); + exdate = (ExDate)event.getProperty(Property.EXDATE); + + if (event.getSummary() != null) + summary = event.getSummary().getValue(); + if (event.getLocation() != null) + location = event.getLocation().getValue(); + if (event.getDescription() != null) + description = event.getDescription().getValue(); + + status = event.getStatus(); + + opaque = true; + if (event.getTransparency() == Transp.TRANSPARENT) + opaque = false; + + organizer = event.getOrganizer(); + for (Object o : event.getProperties(Property.ATTENDEE)) + attendees.add((Attendee)o); + + Clazz classification = event.getClassification(); + if (classification != null) { + if (classification == Clazz.PUBLIC) + forPublic = true; + else if (classification == Clazz.CONFIDENTIAL || classification == Clazz.PRIVATE) + forPublic = false; + } + + this.alarms = event.getAlarms(); + } + + @Override + @SuppressWarnings("unchecked") + public ByteArrayOutputStream toEntity() throws IOException { + net.fortuna.ical4j.model.Calendar ical = new net.fortuna.ical4j.model.Calendar(); + ical.getProperties().add(Version.VERSION_2_0); + ical.getProperties().add(new ProdId("-//bitfire web engineering//DAVdroid " + Constants.APP_VERSION + " (ical4j 1.0.x)//EN")); + + VEvent event = new VEvent(); + PropertyList props = event.getProperties(); + + if (uid != null) + props.add(new Uid(uid)); + + props.add(dtStart); + if (dtEnd != null) + props.add(dtEnd); + if (duration != null) + props.add(duration); + + if (rrule != null) + props.add(rrule); + if (rdate != null) + props.add(rdate); + if (exrule != null) + props.add(exrule); + if (exdate != null) + props.add(exdate); + + if (summary != null && !summary.isEmpty()) + props.add(new Summary(summary)); + if (location != null && !location.isEmpty()) + props.add(new Location(location)); + if (description != null && !description.isEmpty()) + props.add(new Description(description)); + + if (status != null) + props.add(status); + if (!opaque) + props.add(Transp.TRANSPARENT); + + if (organizer != null) + props.add(organizer); + props.addAll(attendees); + + if (forPublic != null) + event.getProperties().add(forPublic ? Clazz.PUBLIC : Clazz.PRIVATE); + + event.getAlarms().addAll(alarms); + + props.add(new LastModified()); + ical.getComponents().add(event); + + // add VTIMEZONE components + net.fortuna.ical4j.model.TimeZone + tzStart = (dtStart == null ? null : dtStart.getTimeZone()), + tzEnd = (dtEnd == null ? null : dtEnd.getTimeZone()); + if (tzStart != null) + ical.getComponents().add(tzStart.getVTimeZone()); + if (tzEnd != null && tzEnd != tzStart) + ical.getComponents().add(tzEnd.getVTimeZone()); + + CalendarOutputter output = new CalendarOutputter(false); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try { + output.output(ical, os); + } catch (ValidationException e) { + Log.e(TAG, "Generated invalid iCalendar"); + } + return os; + } + + + public long getDtStartInMillis() { + return dtStart.getDate().getTime(); + } + + public String getDtStartTzID() { + return getTzId(dtStart); + } + + public void setDtStart(long tsStart, String tzID) { + if (tzID == null) { // all-day + dtStart = new DtStart(new Date(tsStart)); + } else { + DateTime start = new DateTime(tsStart); + start.setTimeZone(tzRegistry.getTimeZone(tzID)); + dtStart = new DtStart(start); + } + } + + + public long getDtEndInMillis() { + return dtEnd.getDate().getTime(); + } + + public String getDtEndTzID() { + return getTzId(dtEnd); + } + + public void setDtEnd(long tsEnd, String tzID) { + if (tzID == null) { // all-day + dtEnd = new DtEnd(new Date(tsEnd)); + } else { + DateTime end = new DateTime(tsEnd); + end.setTimeZone(tzRegistry.getTimeZone(tzID)); + dtEnd = new DtEnd(end); + } + } + + + // helpers + + public boolean isAllDay() { + return !hasTime(dtStart); + } + + protected static boolean hasTime(DateProperty date) { + return date.getDate() instanceof DateTime; + } + + protected static String getTzId(DateProperty date) { + if (date.isUtc() || !hasTime(date)) + return Time.TIMEZONE_UTC; + else if (date.getTimeZone() != null) + return date.getTimeZone().getID(); + else if (date.getParameter(Value.TZID) != null) + return date.getParameter(Value.TZID).getValue(); + + // fallback + return Time.TIMEZONE_UTC; + } + + /* guess matching Android timezone ID */ + protected static void validateTimeZone(DateProperty date) { + if (date.isUtc() || !hasTime(date)) + return; + + String tzID = getTzId(date); + if (tzID == null) + return; + + String localTZ = Time.TIMEZONE_UTC; + + String availableTZs[] = SimpleTimeZone.getAvailableIDs(); + for (String availableTZ : availableTZs) + if (tzID.indexOf(availableTZ, 0) != -1) { + localTZ = availableTZ; + break; + } + + Log.d(TAG, "Assuming time zone " + localTZ + " for " + tzID); + date.setTimeZone(tzRegistry.getTimeZone(localTZ)); + } + + public static String TimezoneDefToTzId(String timezoneDef) throws IllegalArgumentException { + try { + if (timezoneDef != null) { + CalendarBuilder builder = new CalendarBuilder(); + net.fortuna.ical4j.model.Calendar cal = builder.build(new StringReader(timezoneDef)); + VTimeZone timezone = (VTimeZone)cal.getComponent(VTimeZone.VTIMEZONE); + return timezone.getTimeZoneId().getValue(); + } + } catch (Exception ex) { + Log.w(TAG, "Can't understand time zone definition, ignoring", ex); + } + throw new IllegalArgumentException(); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/InvalidResourceException.java b/app/src/main/java/at/bitfire/davdroid/resource/InvalidResourceException.java new file mode 100644 index 00000000..ee24e1f6 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/InvalidResourceException.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.resource; + +public class InvalidResourceException extends Exception { + private static final long serialVersionUID = 1593585432655578220L; + + public InvalidResourceException(String message) { + super(message); + } + + public InvalidResourceException(Throwable throwable) { + super(throwable); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.java new file mode 100644 index 00000000..b8711cbb --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.java @@ -0,0 +1,1007 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.resource; + +import java.io.IOException; +import java.io.InputStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import lombok.Cleanup; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.WordUtils; + +import android.accounts.Account; +import android.content.ContentProviderClient; +import android.content.ContentProviderOperation; +import android.content.ContentProviderOperation.Builder; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.net.Uri; +import android.os.RemoteException; +import android.provider.ContactsContract.CommonDataKinds; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.GroupMembership; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.CommonDataKinds.Nickname; +import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.SipAddress; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.Groups; +import android.provider.ContactsContract.RawContacts; +import android.util.Log; +import at.bitfire.davdroid.syncadapter.AccountSettings; +import ezvcard.parameter.AddressType; +import ezvcard.parameter.EmailType; +import ezvcard.parameter.ImppType; +import ezvcard.parameter.TelephoneType; +import ezvcard.property.Address; +import ezvcard.property.Anniversary; +import ezvcard.property.Birthday; +import ezvcard.property.DateOrTimeProperty; +import ezvcard.property.Impp; +import ezvcard.property.Telephone; + + +public class LocalAddressBook extends LocalCollection { + private final static String TAG = "davdroid.LocalAddressBook"; + + protected final static String COLUMN_UNKNOWN_PROPERTIES = RawContacts.SYNC3; + + + protected AccountSettings accountSettings; + + + /* database fields */ + + @Override + protected Uri entriesURI() { + return syncAdapterURI(RawContacts.CONTENT_URI); + } + + protected String entryColumnAccountType() { return RawContacts.ACCOUNT_TYPE; } + protected String entryColumnAccountName() { return RawContacts.ACCOUNT_NAME; } + + protected String entryColumnParentID() { return null; /* maybe use RawContacts.DATA_SET some day? */ } + protected String entryColumnID() { return RawContacts._ID; } + protected String entryColumnRemoteName() { return RawContacts.SOURCE_ID; } + protected String entryColumnETag() { return RawContacts.SYNC2; } + + protected String entryColumnDirty() { return RawContacts.DIRTY; } + protected String entryColumnDeleted() { return RawContacts.DELETED; } + + protected String entryColumnUID() { return RawContacts.SYNC1; } + + + + public LocalAddressBook(Account account, ContentProviderClient providerClient, AccountSettings accountSettings) { + super(account, providerClient); + this.accountSettings = accountSettings; + } + + + /* collection operations */ + + @Override + public long getId() { + return -1; + } + + @Override + public String getCTag() { + return accountSettings.getAddressBookCTag(); + } + + @Override + public void setCTag(String cTag) { + accountSettings.setAddressBookCTag(cTag); + } + + + /* create/update/delete */ + + public Contact newResource(long localID, String resourceName, String eTag) { + return new Contact(localID, resourceName, eTag); + } + + public void deleteAllExceptRemoteNames(Resource[] remoteResources) { + String where; + + if (remoteResources.length != 0) { + List sqlFileNames = new LinkedList(); + for (Resource res : remoteResources) + sqlFileNames.add(DatabaseUtils.sqlEscapeString(res.getName())); + where = entryColumnRemoteName() + " NOT IN (" + StringUtils.join(sqlFileNames, ",") + ")"; + } else + where = entryColumnRemoteName() + " IS NOT NULL"; + + Builder builder = ContentProviderOperation.newDelete(entriesURI()).withSelection(where, null); + pendingOperations.add(builder + .withYieldAllowed(true) + .build()); + } + + @Override + public void commit() throws LocalStorageException { + super.commit(); + + // update group details for groups we have just created + Uri groupsUri = syncAdapterURI(Groups.CONTENT_URI); + try { + // newly created groups don't have a TITLE + @Cleanup Cursor cursor = providerClient.query(groupsUri, + new String[] { Groups.SOURCE_ID }, + Groups.TITLE + " IS NULL", null, null + ); + while (cursor != null && cursor.moveToNext()) { + // found group, set TITLE to SOURCE_ID and other details + String sourceID = cursor.getString(0); + pendingOperations.add(ContentProviderOperation.newUpdate(groupsUri) + .withSelection(Groups.SOURCE_ID + "=?", new String[] { sourceID }) + .withValue(Groups.TITLE, sourceID) + .withValue(Groups.GROUP_VISIBLE, 1) + .build()); + super.commit(); + } + } catch (RemoteException e) { + throw new LocalStorageException("Couldn't update group names", e); + } + } + + + /* methods for populating the data object from the content provider */ + + @Override + public void populate(Resource res) throws LocalStorageException { + Contact c = (Contact)res; + + try { + @Cleanup Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), c.getLocalID()), + new String[] { entryColumnUID(), COLUMN_UNKNOWN_PROPERTIES, RawContacts.STARRED }, null, null, null); + if (cursor != null && cursor.moveToNext()) { + c.setUid(cursor.getString(0)); + c.setUnknownProperties(cursor.getString(1)); + c.setStarred(cursor.getInt(2) != 0); + } else + throw new RecordNotFoundException(); + + populateStructuredName(c); + populatePhoneNumbers(c); + populateEmailAddresses(c); + populatePhoto(c); + populateOrganization(c); + populateIMPPs(c); + populateNickname(c); + populateNote(c); + populatePostalAddresses(c); + populateCategories(c); + populateURLs(c); + populateEvents(c); + populateSipAddress(c); + } catch(RemoteException ex) { + throw new LocalStorageException(ex); + } + } + + private void populateStructuredName(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { + /* 0 */ StructuredName.DISPLAY_NAME, StructuredName.PREFIX, StructuredName.GIVEN_NAME, + /* 3 */ StructuredName.MIDDLE_NAME, StructuredName.FAMILY_NAME, StructuredName.SUFFIX, + /* 6 */ StructuredName.PHONETIC_GIVEN_NAME, StructuredName.PHONETIC_MIDDLE_NAME, StructuredName.PHONETIC_FAMILY_NAME + }, StructuredName.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), StructuredName.CONTENT_ITEM_TYPE }, null); + + if (cursor != null && cursor.moveToNext()) { + c.setDisplayName(cursor.getString(0)); + + c.setPrefix(cursor.getString(1)); + c.setGivenName(cursor.getString(2)); + c.setMiddleName(cursor.getString(3)); + c.setFamilyName(cursor.getString(4)); + c.setSuffix(cursor.getString(5)); + + c.setPhoneticGivenName(cursor.getString(6)); + c.setPhoneticMiddleName(cursor.getString(7)); + c.setPhoneticFamilyName(cursor.getString(8)); + } + } + + protected void populatePhoneNumbers(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { Phone.TYPE, Phone.LABEL, Phone.NUMBER, Phone.IS_SUPER_PRIMARY }, + Phone.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), Phone.CONTENT_ITEM_TYPE }, null); + while (cursor != null && cursor.moveToNext()) { + ezvcard.property.Telephone number = new ezvcard.property.Telephone(cursor.getString(2)); + switch (cursor.getInt(0)) { + case Phone.TYPE_HOME: + number.addType(TelephoneType.HOME); + break; + case Phone.TYPE_MOBILE: + number.addType(TelephoneType.CELL); + break; + case Phone.TYPE_WORK: + number.addType(TelephoneType.WORK); + break; + case Phone.TYPE_FAX_WORK: + number.addType(TelephoneType.FAX); + number.addType(TelephoneType.WORK); + break; + case Phone.TYPE_FAX_HOME: + number.addType(TelephoneType.FAX); + number.addType(TelephoneType.HOME); + break; + case Phone.TYPE_PAGER: + number.addType(TelephoneType.PAGER); + break; + case Phone.TYPE_CALLBACK: + number.addType(Contact.PHONE_TYPE_CALLBACK); + break; + case Phone.TYPE_CAR: + number.addType(TelephoneType.CAR); + break; + case Phone.TYPE_COMPANY_MAIN: + number.addType(Contact.PHONE_TYPE_COMPANY_MAIN); + break; + case Phone.TYPE_ISDN: + number.addType(TelephoneType.ISDN); + break; + case Phone.TYPE_MAIN: + number.addType(TelephoneType.PREF); + break; + case Phone.TYPE_OTHER_FAX: + number.addType(TelephoneType.FAX); + break; + case Phone.TYPE_RADIO: + number.addType(Contact.PHONE_TYPE_RADIO); + break; + case Phone.TYPE_TELEX: + number.addType(TelephoneType.TEXTPHONE); + break; + case Phone.TYPE_TTY_TDD: + number.addType(TelephoneType.TEXT); + break; + case Phone.TYPE_WORK_MOBILE: + number.addType(TelephoneType.CELL); + number.addType(TelephoneType.WORK); + break; + case Phone.TYPE_WORK_PAGER: + number.addType(TelephoneType.PAGER); + number.addType(TelephoneType.WORK); + break; + case Phone.TYPE_ASSISTANT: + number.addType(Contact.PHONE_TYPE_ASSISTANT); + break; + case Phone.TYPE_MMS: + number.addType(Contact.PHONE_TYPE_MMS); + break; + case Phone.TYPE_CUSTOM: + String customType = cursor.getString(1); + if (!StringUtils.isEmpty(customType)) + number.addType(TelephoneType.get(labelToXName(customType))); + } + if (cursor.getInt(3) != 0) // IS_PRIMARY + number.addType(TelephoneType.PREF); + c.getPhoneNumbers().add(number); + } + } + + protected void populateEmailAddresses(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { Email.TYPE, Email.ADDRESS, Email.LABEL, Email.IS_SUPER_PRIMARY }, + Email.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), Email.CONTENT_ITEM_TYPE }, null); + while (cursor != null && cursor.moveToNext()) { + ezvcard.property.Email email = new ezvcard.property.Email(cursor.getString(1)); + switch (cursor.getInt(0)) { + case Email.TYPE_HOME: + email.addType(EmailType.HOME); + break; + case Email.TYPE_WORK: + email.addType(EmailType.WORK); + break; + case Email.TYPE_MOBILE: + email.addType(Contact.EMAIL_TYPE_MOBILE); + break; + case Email.TYPE_CUSTOM: + String customType = cursor.getString(2); + if (!StringUtils.isEmpty(customType)) + email.addType(EmailType.get(labelToXName(customType))); + } + if (cursor.getInt(3) != 0) // IS_PRIMARY + email.addType(EmailType.PREF); + c.getEmails().add(email); + } + } + + protected void populatePhoto(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), + new String[] { Photo.PHOTO_FILE_ID, Photo.PHOTO }, + Photo.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), Photo.CONTENT_ITEM_TYPE }, null); + if (cursor != null && cursor.moveToNext()) { + if (!cursor.isNull(0)) { + Uri photoUri = Uri.withAppendedPath( + ContentUris.withAppendedId(RawContacts.CONTENT_URI, c.getLocalID()), + RawContacts.DisplayPhoto.CONTENT_DIRECTORY); + try { + @Cleanup AssetFileDescriptor fd = providerClient.openAssetFile(photoUri, "r"); + @Cleanup InputStream is = fd.createInputStream(); + c.setPhoto(IOUtils.toByteArray(is)); + } catch(IOException ex) { + Log.w(TAG, "Couldn't read high-res contact photo", ex); + } + } else + c.setPhoto(cursor.getBlob(1)); + } + } + + protected void populateOrganization(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), + new String[] { Organization.COMPANY, Organization.DEPARTMENT, Organization.TITLE, Organization.JOB_DESCRIPTION }, + Organization.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), Organization.CONTENT_ITEM_TYPE }, null); + if (cursor != null && cursor.moveToNext()) { + String company = cursor.getString(0), + department = cursor.getString(1), + title = cursor.getString(2), + role = cursor.getString(3); + if (!StringUtils.isEmpty(company) || !StringUtils.isEmpty(department)) { + ezvcard.property.Organization org = new ezvcard.property.Organization(); + if (!StringUtils.isEmpty(company)) + org.addValue(company); + if (!StringUtils.isEmpty(department)) + org.addValue(department); + c.setOrganization(org); + } + if (!StringUtils.isEmpty(title)) + c.setJobTitle(title); + if (!StringUtils.isEmpty(role)) + c.setJobDescription(role); + } + } + + protected void populateIMPPs(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { Im.DATA, Im.TYPE, Im.LABEL, Im.PROTOCOL, Im.CUSTOM_PROTOCOL }, + Im.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), Im.CONTENT_ITEM_TYPE }, null); + while (cursor != null && cursor.moveToNext()) { + String handle = cursor.getString(0); + + Impp impp = null; + switch (cursor.getInt(3)) { + case Im.PROTOCOL_AIM: + impp = Impp.aim(handle); + break; + case Im.PROTOCOL_MSN: + impp = Impp.msn(handle); + break; + case Im.PROTOCOL_YAHOO: + impp = Impp.yahoo(handle); + break; + case Im.PROTOCOL_SKYPE: + impp = Impp.skype(handle); + break; + case Im.PROTOCOL_QQ: + impp = new Impp("qq", handle); + break; + case Im.PROTOCOL_GOOGLE_TALK: + impp = new Impp("google-talk", handle); + break; + case Im.PROTOCOL_ICQ: + impp = Impp.icq(handle); + break; + case Im.PROTOCOL_JABBER: + impp = Impp.xmpp(handle); + break; + case Im.PROTOCOL_NETMEETING: + impp = new Impp("netmeeting", handle); + break; + case Im.PROTOCOL_CUSTOM: + impp = new Impp(cursor.getString(4), handle); + } + + if (impp != null) { + switch (cursor.getInt(1)) { + case Im.TYPE_HOME: + impp.addType(ImppType.HOME); + break; + case Im.TYPE_WORK: + impp.addType(ImppType.WORK); + break; + case Im.TYPE_CUSTOM: + String customType = cursor.getString(2); + if (!StringUtils.isEmpty(customType)) + impp.addType(ImppType.get(labelToXName(customType))); + } + c.getImpps().add(impp); + } + } + } + + protected void populateNickname(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { Nickname.NAME }, + Nickname.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), Nickname.CONTENT_ITEM_TYPE }, null); + if (cursor != null && cursor.moveToNext()) + c.setNickName(cursor.getString(0)); + } + + protected void populateNote(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { Note.NOTE }, + Note.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), Note.CONTENT_ITEM_TYPE }, null); + if (cursor != null && cursor.moveToNext()) + c.setNote(cursor.getString(0)); + } + + protected void populatePostalAddresses(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { + /* 0 */ StructuredPostal.FORMATTED_ADDRESS, StructuredPostal.TYPE, StructuredPostal.LABEL, + /* 3 */ StructuredPostal.STREET, StructuredPostal.POBOX, StructuredPostal.NEIGHBORHOOD, + /* 6 */ StructuredPostal.CITY, StructuredPostal.REGION, StructuredPostal.POSTCODE, + /* 9 */ StructuredPostal.COUNTRY + }, StructuredPostal.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), StructuredPostal.CONTENT_ITEM_TYPE }, null); + while (cursor != null && cursor.moveToNext()) { + Address address = new Address(); + + address.setLabel(cursor.getString(0)); + switch (cursor.getInt(1)) { + case StructuredPostal.TYPE_HOME: + address.addType(AddressType.HOME); + break; + case StructuredPostal.TYPE_WORK: + address.addType(AddressType.WORK); + break; + case StructuredPostal.TYPE_CUSTOM: + String customType = cursor.getString(2); + if (!StringUtils.isEmpty(customType)) + address.addType(AddressType.get(labelToXName(customType))); + break; + } + address.setStreetAddress(cursor.getString(3)); + address.setPoBox(cursor.getString(4)); + address.setExtendedAddress(cursor.getString(5)); + address.setLocality(cursor.getString(6)); + address.setRegion(cursor.getString(7)); + address.setPostalCode(cursor.getString(8)); + address.setCountry(cursor.getString(9)); + c.getAddresses().add(address); + } + } + + protected void populateCategories(Contact c) throws RemoteException { + @Cleanup Cursor cursorMemberships = providerClient.query(dataURI(), + new String[] { GroupMembership.GROUP_ROW_ID, GroupMembership.GROUP_SOURCE_ID }, + GroupMembership.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), GroupMembership.CONTENT_ITEM_TYPE }, null); + List categories = c.getCategories(); + while (cursorMemberships != null && cursorMemberships.moveToNext()) { + long rowID = cursorMemberships.getLong(0); + String sourceID = cursorMemberships.getString(1); + + // either a row ID or a source ID must be available + String where, whereArg; + if (sourceID == null) { + where = Groups._ID + "=?"; + whereArg = String.valueOf(rowID); + } else { + where = Groups.SOURCE_ID + "=?"; + whereArg = sourceID; + } + where += " AND " + Groups.DELETED + "=0"; // ignore deleted groups + Log.d(TAG, "Populating group from " + where + " " + whereArg); + + // fetch group + @Cleanup Cursor cursorGroups = providerClient.query(Groups.CONTENT_URI, + new String[] { Groups.TITLE }, + where, new String[] { whereArg }, null + ); + if (cursorGroups != null && cursorGroups.moveToNext()) { + String title = cursorGroups.getString(0); + + if (sourceID == null) { // Group wasn't created by DAVdroid + // SOURCE_ID IS NULL <=> _ID IS NOT NULL + Log.d(TAG, "Setting SOURCE_ID of non-DAVdroid group to title: " + title); + + ContentValues v = new ContentValues(1); + v.put(Groups.SOURCE_ID, title); + v.put(Groups.GROUP_IS_READ_ONLY, 0); + v.put(Groups.GROUP_VISIBLE, 1); + providerClient.update(syncAdapterURI(Groups.CONTENT_URI), v, Groups._ID + "=?", new String[] { String.valueOf(rowID) }); + + sourceID = title; + } + + // add group to CATEGORIES + if (sourceID != null) + categories.add(sourceID); + } else + Log.d(TAG, "Group not found (maybe deleted)"); + } + } + + protected void populateURLs(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { Website.URL }, + Website.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), Website.CONTENT_ITEM_TYPE }, null); + while (cursor != null && cursor.moveToNext()) + c.getURLs().add(cursor.getString(0)); + } + + protected void populateEvents(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), new String[] { CommonDataKinds.Event.TYPE, CommonDataKinds.Event.START_DATE }, + Photo.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), CommonDataKinds.Event.CONTENT_ITEM_TYPE }, null); + while (cursor != null && cursor.moveToNext()) { + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + try { + Date date = formatter.parse(cursor.getString(1)); + switch (cursor.getInt(0)) { + case CommonDataKinds.Event.TYPE_ANNIVERSARY: + c.setAnniversary(new Anniversary(date)); + break; + case CommonDataKinds.Event.TYPE_BIRTHDAY: + c.setBirthDay(new Birthday(date)); + break; + } + } catch (ParseException e) { + Log.w(TAG, "Couldn't parse local birthday/anniversary date", e); + } + } + } + + protected void populateSipAddress(Contact c) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(dataURI(), + new String[] { SipAddress.SIP_ADDRESS, SipAddress.TYPE, SipAddress.LABEL }, + SipAddress.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", + new String[] { String.valueOf(c.getLocalID()), SipAddress.CONTENT_ITEM_TYPE }, null); + if (cursor != null && cursor.moveToNext()) { + Impp impp = new Impp("sip:" + cursor.getString(0)); + switch (cursor.getInt(1)) { + case SipAddress.TYPE_HOME: + impp.addType(ImppType.HOME); + break; + case SipAddress.TYPE_WORK: + impp.addType(ImppType.WORK); + break; + case SipAddress.TYPE_CUSTOM: + String customType = cursor.getString(2); + if (!StringUtils.isEmpty(customType)) + impp.addType(ImppType.get(labelToXName(customType))); + } + c.getImpps().add(impp); + } + } + + + /* content builder methods */ + + @Override + protected Builder buildEntry(Builder builder, Resource resource) { + Contact contact = (Contact)resource; + + return builder + .withValue(RawContacts.ACCOUNT_NAME, account.name) + .withValue(RawContacts.ACCOUNT_TYPE, account.type) + .withValue(entryColumnRemoteName(), contact.getName()) + .withValue(entryColumnUID(), contact.getUid()) + .withValue(entryColumnETag(), contact.getETag()) + .withValue(COLUMN_UNKNOWN_PROPERTIES, contact.getUnknownProperties()) + .withValue(RawContacts.STARRED, contact.isStarred() ? 1 : 0); + } + + + @Override + protected void addDataRows(Resource resource, long localID, int backrefIdx) { + Contact contact = (Contact)resource; + + queueOperation(buildStructuredName(newDataInsertBuilder(localID, backrefIdx), contact)); + + for (Telephone number : contact.getPhoneNumbers()) + queueOperation(buildPhoneNumber(newDataInsertBuilder(localID, backrefIdx), number)); + + for (ezvcard.property.Email email : contact.getEmails()) + queueOperation(buildEmail(newDataInsertBuilder(localID, backrefIdx), email)); + + if (contact.getPhoto() != null) + queueOperation(buildPhoto(newDataInsertBuilder(localID, backrefIdx), contact.getPhoto())); + + queueOperation(buildOrganization(newDataInsertBuilder(localID, backrefIdx), contact)); + + for (Impp impp : contact.getImpps()) + queueOperation(buildIMPP(newDataInsertBuilder(localID, backrefIdx), impp)); + + if (contact.getNickName() != null) + queueOperation(buildNickName(newDataInsertBuilder(localID, backrefIdx), contact.getNickName())); + + if (contact.getNote() != null) + queueOperation(buildNote(newDataInsertBuilder(localID, backrefIdx), contact.getNote())); + + for (Address address : contact.getAddresses()) + queueOperation(buildAddress(newDataInsertBuilder(localID, backrefIdx), address)); + + for (String category : contact.getCategories()) + queueOperation(buildGroupMembership(newDataInsertBuilder(localID, backrefIdx), category)); + + for (String url : contact.getURLs()) + queueOperation(buildURL(newDataInsertBuilder(localID, backrefIdx), url)); + + // events + if (contact.getAnniversary() != null) + queueOperation(buildEvent(newDataInsertBuilder(localID, backrefIdx), contact.getAnniversary(), CommonDataKinds.Event.TYPE_ANNIVERSARY)); + if (contact.getBirthDay() != null) + queueOperation(buildEvent(newDataInsertBuilder(localID, backrefIdx), contact.getBirthDay(), CommonDataKinds.Event.TYPE_BIRTHDAY)); + + // TODO relations + + // SIP addresses are built by buildIMPP + } + + @Override + protected void removeDataRows(Resource resource) { + pendingOperations.add(ContentProviderOperation.newDelete(dataURI()) + .withSelection(Data.RAW_CONTACT_ID + "=?", + new String[] { String.valueOf(resource.getLocalID()) }).build()); + } + + + protected Builder buildStructuredName(Builder builder, Contact contact) { + return builder + .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE) + .withValue(StructuredName.PREFIX, contact.getPrefix()) + .withValue(StructuredName.DISPLAY_NAME, contact.getDisplayName()) + .withValue(StructuredName.GIVEN_NAME, contact.getGivenName()) + .withValue(StructuredName.MIDDLE_NAME, contact.getMiddleName()) + .withValue(StructuredName.FAMILY_NAME, contact.getFamilyName()) + .withValue(StructuredName.SUFFIX, contact.getSuffix()) + .withValue(StructuredName.PHONETIC_GIVEN_NAME, contact.getPhoneticGivenName()) + .withValue(StructuredName.PHONETIC_MIDDLE_NAME, contact.getPhoneticMiddleName()) + .withValue(StructuredName.PHONETIC_FAMILY_NAME, contact.getPhoneticFamilyName()); + } + + protected Builder buildPhoneNumber(Builder builder, Telephone number) { + int typeCode = Phone.TYPE_OTHER; + String typeLabel = null; + boolean is_primary = false; + + Set types = number.getTypes(); + // preferred number? + if (types.contains(TelephoneType.PREF)) + is_primary = true; + + // 1 Android type <-> 2 VCard types: fax, cell, pager + if (types.contains(TelephoneType.FAX)) { + if (types.contains(TelephoneType.HOME)) + typeCode = Phone.TYPE_FAX_HOME; + else if (types.contains(TelephoneType.WORK)) + typeCode = Phone.TYPE_FAX_WORK; + else + typeCode = Phone.TYPE_OTHER_FAX; + } else if (types.contains(TelephoneType.CELL)) { + if (types.contains(TelephoneType.WORK)) + typeCode = Phone.TYPE_WORK_MOBILE; + else + typeCode = Phone.TYPE_MOBILE; + } else if (types.contains(TelephoneType.PAGER)) { + if (types.contains(TelephoneType.WORK)) + typeCode = Phone.TYPE_WORK_PAGER; + else + typeCode = Phone.TYPE_PAGER; + // types with 1:1 translation + } else if (types.contains(TelephoneType.HOME)) { + typeCode = Phone.TYPE_HOME; + } else if (types.contains(TelephoneType.WORK)) { + typeCode = Phone.TYPE_WORK; + } else if (types.contains(Contact.PHONE_TYPE_CALLBACK)) { + typeCode = Phone.TYPE_CALLBACK; + } else if (types.contains(TelephoneType.CAR)) { + typeCode = Phone.TYPE_CAR; + } else if (types.contains(Contact.PHONE_TYPE_COMPANY_MAIN)) { + typeCode = Phone.TYPE_COMPANY_MAIN; + } else if (types.contains(TelephoneType.ISDN)) { + typeCode = Phone.TYPE_ISDN; + } else if (types.contains(TelephoneType.PREF)) { + typeCode = Phone.TYPE_MAIN; + } else if (types.contains(Contact.PHONE_TYPE_RADIO)) { + typeCode = Phone.TYPE_RADIO; + } else if (types.contains(TelephoneType.TEXTPHONE)) { + typeCode = Phone.TYPE_TELEX; + } else if (types.contains(TelephoneType.TEXT)) { + typeCode = Phone.TYPE_TTY_TDD; + } else if (types.contains(Contact.PHONE_TYPE_ASSISTANT)) { + typeCode = Phone.TYPE_ASSISTANT; + } else if (types.contains(Contact.PHONE_TYPE_MMS)) { + typeCode = Phone.TYPE_MMS; + } else if (!types.isEmpty()) { + TelephoneType type = types.iterator().next(); + typeCode = Phone.TYPE_CUSTOM; + typeLabel = xNameToLabel(type.getValue()); + } + + builder = builder + .withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE) + .withValue(Phone.NUMBER, number.getText()) + .withValue(Phone.TYPE, typeCode) + .withValue(Phone.IS_PRIMARY, is_primary ? 1 : 0) + .withValue(Phone.IS_SUPER_PRIMARY, is_primary ? 1 : 0); + if (typeLabel != null) + builder = builder.withValue(Phone.LABEL, typeLabel); + return builder; + } + + protected Builder buildEmail(Builder builder, ezvcard.property.Email email) { + int typeCode = 0; + String typeLabel = null; + boolean is_primary = false; + + for (EmailType type : email.getTypes()) + if (type == EmailType.PREF) + is_primary = true; + else if (type == EmailType.HOME) + typeCode = Email.TYPE_HOME; + else if (type == EmailType.WORK) + typeCode = Email.TYPE_WORK; + else if (type == Contact.EMAIL_TYPE_MOBILE) + typeCode = Email.TYPE_MOBILE; + if (typeCode == 0) { + if (email.getTypes().isEmpty()) + typeCode = Email.TYPE_OTHER; + else { + typeCode = Email.TYPE_CUSTOM; + typeLabel = xNameToLabel(email.getTypes().iterator().next().getValue()); + } + } + + builder = builder + .withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE) + .withValue(Email.ADDRESS, email.getValue()) + .withValue(Email.TYPE, typeCode) + .withValue(Email.IS_PRIMARY, is_primary ? 1 : 0) + .withValue(Phone.IS_SUPER_PRIMARY, is_primary ? 1 : 0);; + if (typeLabel != null) + builder = builder.withValue(Email.LABEL, typeLabel); + return builder; + } + + protected Builder buildPhoto(Builder builder, byte[] photo) { + return builder + .withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE) + .withValue(Photo.PHOTO, photo); + } + + protected Builder buildOrganization(Builder builder, Contact contact) { + if (contact.getOrganization() == null && contact.getJobTitle() == null && contact.getJobDescription() == null) + return null; + + ezvcard.property.Organization organization = contact.getOrganization(); + String company = null, department = null; + if (organization != null) { + Iterator org = organization.getValues().iterator(); + if (org.hasNext()) + company = org.next(); + if (org.hasNext()) + department = org.next(); + } + + return builder + .withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE) + .withValue(Organization.COMPANY, company) + .withValue(Organization.DEPARTMENT, department) + .withValue(Organization.TITLE, contact.getJobTitle()) + .withValue(Organization.JOB_DESCRIPTION, contact.getJobDescription()); + } + + protected Builder buildIMPP(Builder builder, Impp impp) { + int typeCode = 0; + String typeLabel = null; + for (ImppType type : impp.getTypes()) + if (type == ImppType.HOME) + typeCode = Im.TYPE_HOME; + else if (type == ImppType.WORK || type == ImppType.BUSINESS) + typeCode = Im.TYPE_WORK; + if (typeCode == 0) + if (impp.getTypes().isEmpty()) + typeCode = Im.TYPE_OTHER; + else { + typeCode = Im.TYPE_CUSTOM; + typeLabel = xNameToLabel(impp.getTypes().iterator().next().getValue()); + } + + int protocolCode = 0; + String protocolLabel = null; + + String protocol = impp.getProtocol(); + if (protocol == null) { + Log.w(TAG, "Ignoring IMPP address without protocol"); + return null; + } + + // SIP addresses are IMPP entries in the VCard but locally stored in SipAddress rather than Im + boolean sipAddress = false; + + if (impp.isAim()) + protocolCode = Im.PROTOCOL_AIM; + else if (impp.isMsn()) + protocolCode = Im.PROTOCOL_MSN; + else if (impp.isYahoo()) + protocolCode = Im.PROTOCOL_YAHOO; + else if (impp.isSkype()) + protocolCode = Im.PROTOCOL_SKYPE; + else if (protocol.equalsIgnoreCase("qq")) + protocolCode = Im.PROTOCOL_QQ; + else if (protocol.equalsIgnoreCase("google-talk")) + protocolCode = Im.PROTOCOL_GOOGLE_TALK; + else if (impp.isIcq()) + protocolCode = Im.PROTOCOL_ICQ; + else if (impp.isXmpp() || protocol.equalsIgnoreCase("jabber")) + protocolCode = Im.PROTOCOL_JABBER; + else if (protocol.equalsIgnoreCase("netmeeting")) + protocolCode = Im.PROTOCOL_NETMEETING; + else if (protocol.equalsIgnoreCase("sip")) + sipAddress = true; + else { + protocolCode = Im.PROTOCOL_CUSTOM; + protocolLabel = protocol; + } + + if (sipAddress) + // save as SIP address + builder = builder + .withValue(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE) + .withValue(Im.DATA, impp.getHandle()) + .withValue(Im.TYPE, typeCode); + else { + // save as IM address + builder = builder + .withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE) + .withValue(Im.DATA, impp.getHandle()) + .withValue(Im.TYPE, typeCode) + .withValue(Im.PROTOCOL, protocolCode); + if (protocolLabel != null) + builder = builder.withValue(Im.CUSTOM_PROTOCOL, protocolLabel); + } + if (typeLabel != null) + builder = builder.withValue(Im.LABEL, typeLabel); + return builder; + } + + protected Builder buildNickName(Builder builder, String nickName) { + return builder + .withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE) + .withValue(Nickname.NAME, nickName); + } + + protected Builder buildNote(Builder builder, String note) { + return builder + .withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE) + .withValue(Note.NOTE, note); + } + + protected Builder buildAddress(Builder builder, Address address) { + /* street po.box (extended) + * region + * postal code city + * country + */ + String formattedAddress = address.getLabel(); + if (StringUtils.isEmpty(formattedAddress)) { + String lineStreet = StringUtils.join(new String[] { address.getStreetAddress(), address.getPoBox(), address.getExtendedAddress() }, " "), + lineLocality = StringUtils.join(new String[] { address.getPostalCode(), address.getLocality() }, " "); + + List lines = new LinkedList(); + if (lineStreet != null) + lines.add(lineStreet); + if (address.getRegion() != null && !address.getRegion().isEmpty()) + lines.add(address.getRegion()); + if (lineLocality != null) + lines.add(lineLocality); + + formattedAddress = StringUtils.join(lines, "\n"); + } + + int typeCode = 0; + String typeLabel = null; + for (AddressType type : address.getTypes()) + if (type == AddressType.HOME) + typeCode = StructuredPostal.TYPE_HOME; + else if (type == AddressType.WORK) + typeCode = StructuredPostal.TYPE_WORK; + if (typeCode == 0) + if (address.getTypes().isEmpty()) + typeCode = StructuredPostal.TYPE_OTHER; + else { + typeCode = StructuredPostal.TYPE_CUSTOM; + typeLabel = xNameToLabel(address.getTypes().iterator().next().getValue()); + } + + builder = builder + .withValue(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE) + .withValue(StructuredPostal.FORMATTED_ADDRESS, formattedAddress) + .withValue(StructuredPostal.TYPE, typeCode) + .withValue(StructuredPostal.STREET, address.getStreetAddress()) + .withValue(StructuredPostal.POBOX, address.getPoBox()) + .withValue(StructuredPostal.NEIGHBORHOOD, address.getExtendedAddress()) + .withValue(StructuredPostal.CITY, address.getLocality()) + .withValue(StructuredPostal.REGION, address.getRegion()) + .withValue(StructuredPostal.POSTCODE, address.getPostalCode()) + .withValue(StructuredPostal.COUNTRY, address.getCountry()); + if (typeLabel != null) + builder = builder.withValue(StructuredPostal.LABEL, typeLabel); + return builder; + } + + protected Builder buildGroupMembership(Builder builder, String group) { + return builder + .withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE) + .withValue(GroupMembership.GROUP_SOURCE_ID, group); + } + + protected Builder buildURL(Builder builder, String url) { + return builder + .withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE) + .withValue(Website.URL, url); + } + + protected Builder buildEvent(Builder builder, DateOrTimeProperty date, int type) { + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + if (date.getDate() == null) { + Log.i(TAG, "Ignoring contact event without date"); + return null; + } + return builder + .withValue(Data.MIMETYPE, CommonDataKinds.Event.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.Event.TYPE, type) + .withValue(CommonDataKinds.Event.START_DATE, formatter.format(date.getDate())); + } + + + + /* helper methods */ + + protected Uri dataURI() { + return syncAdapterURI(Data.CONTENT_URI); + } + + protected static String labelToXName(String label) { + return "X-" + label.replaceAll(" ","_").replaceAll("[^\\p{L}\\p{Nd}\\-_]", "").toUpperCase(Locale.US); + } + + private Builder newDataInsertBuilder(long raw_contact_id, Integer backrefIdx) { + return newDataInsertBuilder(dataURI(), Data.RAW_CONTACT_ID, raw_contact_id, backrefIdx); + } + + protected static String xNameToLabel(String xname) { + // "X-MY_PROPERTY" + // 1. ensure lower case -> "x-my_property" + // 2. remove x- from beginning -> "my_property" + // 3. replace "_" by " " -> "my property" + // 4. capitalize -> "My Property" + String lowerCase = StringUtils.lowerCase(xname, Locale.US), + withoutPrefix = StringUtils.removeStart(lowerCase, "x-"), + withSpaces = StringUtils.replace(withoutPrefix, "_", " "); + return WordUtils.capitalize(withSpaces); + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java new file mode 100644 index 00000000..9fb60064 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalCalendar.java @@ -0,0 +1,612 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.resource; + +import java.net.URI; +import java.net.URISyntaxException; +import java.text.ParseException; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import lombok.Cleanup; +import lombok.Getter; +import net.fortuna.ical4j.model.Dur; +import net.fortuna.ical4j.model.Parameter; +import net.fortuna.ical4j.model.ParameterList; +import net.fortuna.ical4j.model.PropertyList; +import net.fortuna.ical4j.model.component.VAlarm; +import net.fortuna.ical4j.model.parameter.Cn; +import net.fortuna.ical4j.model.parameter.CuType; +import net.fortuna.ical4j.model.parameter.PartStat; +import net.fortuna.ical4j.model.parameter.Role; +import net.fortuna.ical4j.model.property.Action; +import net.fortuna.ical4j.model.property.Attendee; +import net.fortuna.ical4j.model.property.Description; +import net.fortuna.ical4j.model.property.Duration; +import net.fortuna.ical4j.model.property.ExDate; +import net.fortuna.ical4j.model.property.ExRule; +import net.fortuna.ical4j.model.property.Organizer; +import net.fortuna.ical4j.model.property.RDate; +import net.fortuna.ical4j.model.property.RRule; +import net.fortuna.ical4j.model.property.Status; + +import org.apache.commons.lang.StringUtils; + +import android.accounts.Account; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.ContentProviderClient; +import android.content.ContentProviderOperation; +import android.content.ContentProviderOperation.Builder; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.net.Uri; +import android.os.Build; +import android.os.RemoteException; +import android.provider.CalendarContract; +import android.provider.CalendarContract.Attendees; +import android.provider.CalendarContract.Calendars; +import android.provider.CalendarContract.Events; +import android.provider.CalendarContract.Reminders; +import android.provider.ContactsContract; +import android.util.Log; + +/** + * Represents a locally stored calendar, containing Events. + * Communicates with the Android Contacts Provider which uses an SQLite + * database to store the contacts. + */ +public class LocalCalendar extends LocalCollection { + private static final String TAG = "davdroid.LocalCalendar"; + + @Getter protected long id; + @Getter protected String url; + + protected static String COLLECTION_COLUMN_CTAG = Calendars.CAL_SYNC1; + + + /* database fields */ + + @Override + protected Uri entriesURI() { + return syncAdapterURI(Events.CONTENT_URI); + } + + protected String entryColumnAccountType() { return Events.ACCOUNT_TYPE; } + protected String entryColumnAccountName() { return Events.ACCOUNT_NAME; } + + protected String entryColumnParentID() { return Events.CALENDAR_ID; } + protected String entryColumnID() { return Events._ID; } + protected String entryColumnRemoteName() { return Events._SYNC_ID; } + protected String entryColumnETag() { return Events.SYNC_DATA1; } + + protected String entryColumnDirty() { return Events.DIRTY; } + protected String entryColumnDeleted() { return Events.DELETED; } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + protected String entryColumnUID() { + return (android.os.Build.VERSION.SDK_INT >= 17) ? + Events.UID_2445 : Events.SYNC_DATA2; + } + + + /* class methods, constructor */ + + @SuppressLint("InlinedApi") + public static void create(Account account, ContentResolver resolver, ServerInfo.ResourceInfo info) throws LocalStorageException { + ContentProviderClient client = resolver.acquireContentProviderClient(CalendarContract.AUTHORITY); + if (client == null) + throw new LocalStorageException("No Calendar Provider found (Calendar app disabled?)"); + + int color = 0xFFC3EA6E; // fallback: "DAVdroid green" + if (info.getColor() != null) { + Pattern p = Pattern.compile("#?(\\p{XDigit}{6})(\\p{XDigit}{2})?"); + Matcher m = p.matcher(info.getColor()); + if (m.find()) { + int color_rgb = Integer.parseInt(m.group(1), 16); + int color_alpha = m.group(2) != null ? (Integer.parseInt(m.group(2), 16) & 0xFF) : 0xFF; + color = (color_alpha << 24) | color_rgb; + } + } + + ContentValues values = new ContentValues(); + values.put(Calendars.ACCOUNT_NAME, account.name); + values.put(Calendars.ACCOUNT_TYPE, account.type); + values.put(Calendars.NAME, info.getURL()); + values.put(Calendars.CALENDAR_DISPLAY_NAME, info.getTitle()); + values.put(Calendars.CALENDAR_COLOR, color); + values.put(Calendars.OWNER_ACCOUNT, account.name); + values.put(Calendars.SYNC_EVENTS, 1); + values.put(Calendars.VISIBLE, 1); + values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT); + + if (info.isReadOnly()) + values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ); + else { + values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER); + values.put(Calendars.CAN_ORGANIZER_RESPOND, 1); + values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1); + } + + if (android.os.Build.VERSION.SDK_INT >= 15) { + values.put(Calendars.ALLOWED_AVAILABILITY, Events.AVAILABILITY_BUSY + "," + Events.AVAILABILITY_FREE + "," + Events.AVAILABILITY_TENTATIVE); + values.put(Calendars.ALLOWED_ATTENDEE_TYPES, Attendees.TYPE_NONE + "," + Attendees.TYPE_OPTIONAL + "," + Attendees.TYPE_REQUIRED + "," + Attendees.TYPE_RESOURCE); + } + + if (info.getTimezone() != null) + values.put(Calendars.CALENDAR_TIME_ZONE, info.getTimezone()); + + Log.i(TAG, "Inserting calendar: " + values.toString() + " -> " + calendarsURI(account).toString()); + try { + client.insert(calendarsURI(account), values); + } catch(RemoteException e) { + throw new LocalStorageException(e); + } + } + + public static LocalCalendar[] findAll(Account account, ContentProviderClient providerClient) throws RemoteException { + @Cleanup Cursor cursor = providerClient.query(calendarsURI(account), + new String[] { Calendars._ID, Calendars.NAME }, + Calendars.DELETED + "=0 AND " + Calendars.SYNC_EVENTS + "=1", null, null); + + LinkedList calendars = new LinkedList(); + while (cursor != null && cursor.moveToNext()) + calendars.add(new LocalCalendar(account, providerClient, cursor.getInt(0), cursor.getString(1))); + return calendars.toArray(new LocalCalendar[0]); + } + + public LocalCalendar(Account account, ContentProviderClient providerClient, long id, String url) throws RemoteException { + super(account, providerClient); + this.id = id; + this.url = url; + } + + + /* collection operations */ + + @Override + public String getCTag() throws LocalStorageException { + try { + @Cleanup Cursor c = providerClient.query(ContentUris.withAppendedId(calendarsURI(), id), + new String[] { COLLECTION_COLUMN_CTAG }, null, null, null); + if (c.moveToFirst()) { + return c.getString(0); + } else + throw new LocalStorageException("Couldn't query calendar CTag"); + } catch(RemoteException e) { + throw new LocalStorageException(e); + } + } + + @Override + public void setCTag(String cTag) throws LocalStorageException { + ContentValues values = new ContentValues(1); + values.put(COLLECTION_COLUMN_CTAG, cTag); + try { + providerClient.update(ContentUris.withAppendedId(calendarsURI(), id), values, null, null); + } catch(RemoteException e) { + throw new LocalStorageException(e); + } + } + + + /* create/update/delete */ + + public Event newResource(long localID, String resourceName, String eTag) { + return new Event(localID, resourceName, eTag); + } + + public void deleteAllExceptRemoteNames(Resource[] remoteResources) { + String where; + + if (remoteResources.length != 0) { + List sqlFileNames = new LinkedList(); + for (Resource res : remoteResources) + sqlFileNames.add(DatabaseUtils.sqlEscapeString(res.getName())); + where = entryColumnRemoteName() + " NOT IN (" + StringUtils.join(sqlFileNames, ",") + ")"; + } else + where = entryColumnRemoteName() + " IS NOT NULL"; + + Builder builder = ContentProviderOperation.newDelete(entriesURI()) + .withSelection(entryColumnParentID() + "=? AND (" + where + ")", new String[] { String.valueOf(id) }); + pendingOperations.add(builder + .withYieldAllowed(true) + .build()); + } + + + /* methods for populating the data object from the content provider */ + + + @Override + public void populate(Resource resource) throws LocalStorageException { + Event e = (Event)resource; + + try { + @Cleanup Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), e.getLocalID()), + new String[] { + /* 0 */ Events.TITLE, Events.EVENT_LOCATION, Events.DESCRIPTION, + /* 3 */ Events.DTSTART, Events.DTEND, Events.EVENT_TIMEZONE, Events.EVENT_END_TIMEZONE, Events.ALL_DAY, + /* 8 */ Events.STATUS, Events.ACCESS_LEVEL, + /* 10 */ Events.RRULE, Events.RDATE, Events.EXRULE, Events.EXDATE, + /* 14 */ Events.HAS_ATTENDEE_DATA, Events.ORGANIZER, Events.SELF_ATTENDEE_STATUS, + /* 17 */ entryColumnUID(), Events.DURATION, Events.AVAILABILITY + }, null, null, null); + if (cursor != null && cursor.moveToNext()) { + e.setUid(cursor.getString(17)); + + e.setSummary(cursor.getString(0)); + e.setLocation(cursor.getString(1)); + e.setDescription(cursor.getString(2)); + + boolean allDay = cursor.getInt(7) != 0; + long tsStart = cursor.getLong(3), + tsEnd = cursor.getLong(4); + String duration = cursor.getString(18); + + String tzId = null; + if (allDay) { + e.setDtStart(tsStart, null); + // provide only DTEND and not DURATION for all-day events + if (tsEnd == 0) { + Dur dur = new Dur(duration); + java.util.Date dEnd = dur.getTime(new java.util.Date(tsStart)); + tsEnd = dEnd.getTime(); + } + e.setDtEnd(tsEnd, null); + + } else { + // use the start time zone for the end time, too + // because apps like Samsung Planner allow the user to change "the" time zone but change the start time zone only + tzId = cursor.getString(5); + e.setDtStart(tsStart, tzId); + if (tsEnd != 0) + e.setDtEnd(tsEnd, tzId); + else if (!StringUtils.isEmpty(duration)) + e.setDuration(new Duration(new Dur(duration))); + } + + // recurrence + try { + String strRRule = cursor.getString(10); + if (!StringUtils.isEmpty(strRRule)) + e.setRrule(new RRule(strRRule)); + + String strRDate = cursor.getString(11); + if (!StringUtils.isEmpty(strRDate)) { + RDate rDate = new RDate(); + rDate.setValue(strRDate); + e.setRdate(rDate); + } + + String strExRule = cursor.getString(12); + if (!StringUtils.isEmpty(strExRule)) { + ExRule exRule = new ExRule(); + exRule.setValue(strExRule); + e.setExrule(exRule); + } + + String strExDate = cursor.getString(13); + if (!StringUtils.isEmpty(strExDate)) { + // ignored, see https://code.google.com/p/android/issues/detail?id=21426 + ExDate exDate = new ExDate(); + exDate.setValue(strExDate); + e.setExdate(exDate); + } + } catch (ParseException ex) { + Log.w(TAG, "Couldn't parse recurrence rules, ignoring", ex); + } catch (IllegalArgumentException ex) { + Log.w(TAG, "Invalid recurrence rules, ignoring", ex); + } + + // status + switch (cursor.getInt(8)) { + case Events.STATUS_CONFIRMED: + e.setStatus(Status.VEVENT_CONFIRMED); + break; + case Events.STATUS_TENTATIVE: + e.setStatus(Status.VEVENT_TENTATIVE); + break; + case Events.STATUS_CANCELED: + e.setStatus(Status.VEVENT_CANCELLED); + } + + // availability + e.setOpaque(cursor.getInt(19) != Events.AVAILABILITY_FREE); + + // attendees + if (cursor.getInt(14) != 0) { // has attendees + try { + e.setOrganizer(new Organizer(new URI("mailto", cursor.getString(15), null))); + } catch (URISyntaxException ex) { + Log.e(TAG, "Error when creating ORGANIZER URI, ignoring", ex); + } + populateAttendees(e); + } + + // classification + switch (cursor.getInt(9)) { + case Events.ACCESS_CONFIDENTIAL: + case Events.ACCESS_PRIVATE: + e.setForPublic(false); + break; + case Events.ACCESS_PUBLIC: + e.setForPublic(true); + } + + populateReminders(e); + } else + throw new RecordNotFoundException(); + } catch(RemoteException ex) { + throw new LocalStorageException(ex); + } + } + + + void populateAttendees(Event e) throws RemoteException { + Uri attendeesUri = Attendees.CONTENT_URI.buildUpon() + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") + .build(); + @Cleanup Cursor c = providerClient.query(attendeesUri, new String[] { + /* 0 */ Attendees.ATTENDEE_EMAIL, Attendees.ATTENDEE_NAME, Attendees.ATTENDEE_TYPE, + /* 3 */ Attendees.ATTENDEE_RELATIONSHIP, Attendees.STATUS + }, Attendees.EVENT_ID + "=?", new String[] { String.valueOf(e.getLocalID()) }, null); + while (c != null && c.moveToNext()) { + try { + Attendee attendee = new Attendee(new URI("mailto", c.getString(0), null)); + ParameterList params = attendee.getParameters(); + + String cn = c.getString(1); + if (cn != null) + params.add(new Cn(cn)); + + // type + int type = c.getInt(2); + params.add((type == Attendees.TYPE_RESOURCE) ? CuType.RESOURCE : CuType.INDIVIDUAL); + + // role + int relationship = c.getInt(3); + switch (relationship) { + case Attendees.RELATIONSHIP_ORGANIZER: + params.add(Role.CHAIR); + break; + case Attendees.RELATIONSHIP_ATTENDEE: + case Attendees.RELATIONSHIP_PERFORMER: + case Attendees.RELATIONSHIP_SPEAKER: + params.add((type == Attendees.TYPE_REQUIRED) ? Role.REQ_PARTICIPANT : Role.OPT_PARTICIPANT); + break; + case Attendees.RELATIONSHIP_NONE: + params.add(Role.NON_PARTICIPANT); + } + + // status + switch (c.getInt(4)) { + case Attendees.ATTENDEE_STATUS_INVITED: + params.add(PartStat.NEEDS_ACTION); + break; + case Attendees.ATTENDEE_STATUS_ACCEPTED: + params.add(PartStat.ACCEPTED); + break; + case Attendees.ATTENDEE_STATUS_DECLINED: + params.add(PartStat.DECLINED); + break; + case Attendees.ATTENDEE_STATUS_TENTATIVE: + params.add(PartStat.TENTATIVE); + break; + } + + e.addAttendee(attendee); + } catch (URISyntaxException ex) { + Log.e(TAG, "Couldn't parse attendee information, ignoring", ex); + } + } + } + + void populateReminders(Event e) throws RemoteException { + // reminders + Uri remindersUri = Reminders.CONTENT_URI.buildUpon() + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") + .build(); + @Cleanup Cursor c = providerClient.query(remindersUri, new String[] { + /* 0 */ Reminders.MINUTES, Reminders.METHOD + }, Reminders.EVENT_ID + "=?", new String[] { String.valueOf(e.getLocalID()) }, null); + while (c != null && c.moveToNext()) { + VAlarm alarm = new VAlarm(new Dur(0, 0, -c.getInt(0), 0)); + + PropertyList props = alarm.getProperties(); + switch (c.getInt(1)) { + /*case Reminders.METHOD_EMAIL: + props.add(Action.EMAIL); + break;*/ + default: + props.add(Action.DISPLAY); + props.add(new Description(e.getSummary())); + } + e.addAlarm(alarm); + } + } + + + /* content builder methods */ + + @Override + protected Builder buildEntry(Builder builder, Resource resource) { + Event event = (Event)resource; + + builder = builder + .withValue(Events.CALENDAR_ID, id) + .withValue(entryColumnRemoteName(), event.getName()) + .withValue(entryColumnETag(), event.getETag()) + .withValue(entryColumnUID(), event.getUid()) + .withValue(Events.ALL_DAY, event.isAllDay() ? 1 : 0) + .withValue(Events.DTSTART, event.getDtStartInMillis()) + .withValue(Events.EVENT_TIMEZONE, event.getDtStartTzID()) + .withValue(Events.HAS_ATTENDEE_DATA, event.getAttendees().isEmpty() ? 0 : 1) + .withValue(Events.GUESTS_CAN_INVITE_OTHERS, 1) + .withValue(Events.GUESTS_CAN_MODIFY, 1) + .withValue(Events.GUESTS_CAN_SEE_GUESTS, 1); + + boolean recurring = false; + if (event.getRrule() != null) { + recurring = true; + builder = builder.withValue(Events.RRULE, event.getRrule().getValue()); + } + if (event.getRdate() != null) { + recurring = true; + builder = builder.withValue(Events.RDATE, event.getRdate().getValue()); + } + if (event.getExrule() != null) + builder = builder.withValue(Events.EXRULE, event.getExrule().getValue()); + if (event.getExdate() != null) + builder = builder.withValue(Events.EXDATE, event.getExdate().getValue()); + + // set either DTEND for single-time events or DURATION for recurring events + // because that's the way Android likes it (see docs) + if (recurring) { + // calculate DURATION from start and end date + Duration duration = new Duration(event.getDtStart().getDate(), event.getDtEnd().getDate()); + builder = builder.withValue(Events.DURATION, duration.getValue()); + } else { + builder = builder + .withValue(Events.DTEND, event.getDtEndInMillis()) + .withValue(Events.EVENT_END_TIMEZONE, event.getDtEndTzID()); + } + + if (event.getSummary() != null) + builder = builder.withValue(Events.TITLE, event.getSummary()); + if (event.getLocation() != null) + builder = builder.withValue(Events.EVENT_LOCATION, event.getLocation()); + if (event.getDescription() != null) + builder = builder.withValue(Events.DESCRIPTION, event.getDescription()); + + if (event.getOrganizer() != null && event.getOrganizer().getCalAddress() != null) { + URI organizer = event.getOrganizer().getCalAddress(); + if (organizer.getScheme() != null && organizer.getScheme().equalsIgnoreCase("mailto")) + builder = builder.withValue(Events.ORGANIZER, organizer.getSchemeSpecificPart()); + } + + Status status = event.getStatus(); + if (status != null) { + int statusCode = Events.STATUS_TENTATIVE; + if (status == Status.VEVENT_CONFIRMED) + statusCode = Events.STATUS_CONFIRMED; + else if (status == Status.VEVENT_CANCELLED) + statusCode = Events.STATUS_CANCELED; + builder = builder.withValue(Events.STATUS, statusCode); + } + + builder = builder.withValue(Events.AVAILABILITY, event.isOpaque() ? Events.AVAILABILITY_BUSY : Events.AVAILABILITY_FREE); + + if (event.getForPublic() != null) + builder = builder.withValue(Events.ACCESS_LEVEL, event.getForPublic() ? Events.ACCESS_PUBLIC : Events.ACCESS_PRIVATE); + + return builder; + } + + + @Override + protected void addDataRows(Resource resource, long localID, int backrefIdx) { + Event event = (Event)resource; + for (Attendee attendee : event.getAttendees()) + pendingOperations.add(buildAttendee(newDataInsertBuilder(Attendees.CONTENT_URI, Attendees.EVENT_ID, localID, backrefIdx), attendee).build()); + for (VAlarm alarm : event.getAlarms()) + pendingOperations.add(buildReminder(newDataInsertBuilder(Reminders.CONTENT_URI, Reminders.EVENT_ID, localID, backrefIdx), alarm).build()); + } + + @Override + protected void removeDataRows(Resource resource) { + Event event = (Event)resource; + pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Attendees.CONTENT_URI)) + .withSelection(Attendees.EVENT_ID + "=?", + new String[] { String.valueOf(event.getLocalID()) }).build()); + pendingOperations.add(ContentProviderOperation.newDelete(syncAdapterURI(Reminders.CONTENT_URI)) + .withSelection(Reminders.EVENT_ID + "=?", + new String[] { String.valueOf(event.getLocalID()) }).build()); + } + + + @SuppressLint("InlinedApi") + protected Builder buildAttendee(Builder builder, Attendee attendee) { + Uri member = Uri.parse(attendee.getValue()); + String email = member.getSchemeSpecificPart(); + + Cn cn = (Cn)attendee.getParameter(Parameter.CN); + if (cn != null) + builder = builder.withValue(Attendees.ATTENDEE_NAME, cn.getValue()); + + int type = Attendees.TYPE_NONE; + + CuType cutype = (CuType)attendee.getParameter(Parameter.CUTYPE); + if (cutype == CuType.RESOURCE) + type = Attendees.TYPE_RESOURCE; + else { + Role role = (Role)attendee.getParameter(Parameter.ROLE); + int relationship; + if (role == Role.CHAIR) + relationship = Attendees.RELATIONSHIP_ORGANIZER; + else { + relationship = Attendees.RELATIONSHIP_ATTENDEE; + if (role == Role.OPT_PARTICIPANT) + type = Attendees.TYPE_OPTIONAL; + else if (role == Role.REQ_PARTICIPANT) + type = Attendees.TYPE_REQUIRED; + } + builder = builder.withValue(Attendees.ATTENDEE_RELATIONSHIP, relationship); + } + + int status = Attendees.ATTENDEE_STATUS_NONE; + PartStat partStat = (PartStat)attendee.getParameter(Parameter.PARTSTAT); + if (partStat == null || partStat == PartStat.NEEDS_ACTION) + status = Attendees.ATTENDEE_STATUS_INVITED; + else if (partStat == PartStat.ACCEPTED) + status = Attendees.ATTENDEE_STATUS_ACCEPTED; + else if (partStat == PartStat.DECLINED) + status = Attendees.ATTENDEE_STATUS_DECLINED; + else if (partStat == PartStat.TENTATIVE) + status = Attendees.ATTENDEE_STATUS_TENTATIVE; + + return builder + .withValue(Attendees.ATTENDEE_EMAIL, email) + .withValue(Attendees.ATTENDEE_TYPE, type) + .withValue(Attendees.ATTENDEE_STATUS, status); + } + + protected Builder buildReminder(Builder builder, VAlarm alarm) { + int minutes = 0; + + Dur duration; + if (alarm.getTrigger() != null && (duration = alarm.getTrigger().getDuration()) != null) + minutes = duration.getDays() * 24*60 + duration.getHours()*60 + duration.getMinutes(); + + Log.d(TAG, "Adding alarm " + minutes + " min before"); + + return builder + .withValue(Reminders.METHOD, Reminders.METHOD_ALERT) + .withValue(Reminders.MINUTES, minutes); + } + + + + /* private helper methods */ + + protected static Uri calendarsURI(Account account) { + return Calendars.CONTENT_URI.buildUpon().appendQueryParameter(Calendars.ACCOUNT_NAME, account.name) + .appendQueryParameter(Calendars.ACCOUNT_TYPE, account.type) + .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").build(); + } + + protected Uri calendarsURI() { + return calendarsURI(account); + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalCollection.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalCollection.java new file mode 100644 index 00000000..24293421 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalCollection.java @@ -0,0 +1,361 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.resource; + +import java.util.ArrayList; + +import lombok.Cleanup; +import android.accounts.Account; +import android.content.ContentProviderClient; +import android.content.ContentProviderOperation; +import android.content.ContentProviderOperation.Builder; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; +import android.provider.CalendarContract; +import android.util.Log; + +/** + * Represents a locally-stored synchronizable collection (for instance, the + * address book or a calendar). Manages a CTag that stores the last known + * remote CTag (the remote CTag changes whenever something in the remote collection changes). + * + * @param Subtype of Resource that can be stored in the collection + */ +public abstract class LocalCollection { + private static final String TAG = "davdroid.LocalCollection"; + + protected Account account; + protected ContentProviderClient providerClient; + protected ArrayList pendingOperations = new ArrayList(); + + + // database fields + + /** base Uri of the collection's entries (for instance, Events.CONTENT_URI); + * apply syncAdapterURI() before returning a value */ + abstract protected Uri entriesURI(); + + /** column name of the type of the account the entry belongs to */ + abstract protected String entryColumnAccountType(); + /** column name of the name of the account the entry belongs to */ + abstract protected String entryColumnAccountName(); + + /** column name of the collection ID the entry belongs to */ + abstract protected String entryColumnParentID(); + /** column name of an entry's ID */ + abstract protected String entryColumnID(); + /** column name of an entry's file name on the WebDAV server */ + abstract protected String entryColumnRemoteName(); + /** column name of an entry's last ETag on the WebDAV server; null if entry hasn't been uploaded yet */ + abstract protected String entryColumnETag(); + + /** column name of an entry's "dirty" flag (managed by content provider) */ + abstract protected String entryColumnDirty(); + /** column name of an entry's "deleted" flag (managed by content provider) */ + abstract protected String entryColumnDeleted(); + + /** column name of an entry's UID */ + abstract protected String entryColumnUID(); + + + LocalCollection(Account account, ContentProviderClient providerClient) { + this.account = account; + this.providerClient = providerClient; + } + + + // collection operations + + /** gets the ID if the collection (for instance, ID of the Android calendar) */ + abstract public long getId(); + /** gets the CTag of the collection */ + abstract public String getCTag() throws LocalStorageException; + /** sets the CTag of the collection */ + abstract public void setCTag(String cTag) throws LocalStorageException; + + + // content provider (= database) querying + + /** + * Finds new resources (resources which haven't been uploaded yet). + * New resources are 1) dirty, and 2) don't have an ETag yet. + * + * @return IDs of new resources + * @throws LocalStorageException when the content provider couldn't be queried + */ + public long[] findNew() throws LocalStorageException { + String where = entryColumnDirty() + "=1 AND " + entryColumnETag() + " IS NULL"; + if (entryColumnParentID() != null) + where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId()); + try { + @Cleanup Cursor cursor = providerClient.query(entriesURI(), + new String[] { entryColumnID() }, + where, null, null); + if (cursor == null) + throw new LocalStorageException("Couldn't query new records"); + + long[] fresh = new long[cursor.getCount()]; + for (int idx = 0; cursor.moveToNext(); idx++) { + long id = cursor.getLong(0); + + // new record: generate UID + remote file name so that we can upload + T resource = findById(id, false); + resource.initialize(); + // write generated UID + remote file name into database + ContentValues values = new ContentValues(2); + values.put(entryColumnUID(), resource.getUid()); + values.put(entryColumnRemoteName(), resource.getName()); + providerClient.update(ContentUris.withAppendedId(entriesURI(), id), values, null, null); + + fresh[idx] = id; + } + return fresh; + } catch(RemoteException ex) { + throw new LocalStorageException(ex); + } + } + + /** + * Finds updated resources (resources which have already been uploaded, but have changed locally). + * Updated resources are 1) dirty, and 2) already have an ETag. + * + * @return IDs of updated resources + * @throws LocalStorageException when the content provider couldn't be queried + */ + public long[] findUpdated() throws LocalStorageException { + String where = entryColumnDirty() + "=1 AND " + entryColumnETag() + " IS NOT NULL"; + if (entryColumnParentID() != null) + where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId()); + try { + @Cleanup Cursor cursor = providerClient.query(entriesURI(), + new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() }, + where, null, null); + if (cursor == null) + throw new LocalStorageException("Couldn't query updated records"); + + long[] updated = new long[cursor.getCount()]; + for (int idx = 0; cursor.moveToNext(); idx++) + updated[idx] = cursor.getLong(0); + return updated; + } catch(RemoteException ex) { + throw new LocalStorageException(ex); + } + } + + /** + * Finds deleted resources (resources which have been marked for deletion). + * Deleted resources have the "deleted" flag set. + * + * @return IDs of deleted resources + * @throws LocalStorageException when the content provider couldn't be queried + */ + public long[] findDeleted() throws LocalStorageException { + String where = entryColumnDeleted() + "=1"; + if (entryColumnParentID() != null) + where += " AND " + entryColumnParentID() + "=" + String.valueOf(getId()); + try { + @Cleanup Cursor cursor = providerClient.query(entriesURI(), + new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() }, + where, null, null); + if (cursor == null) + throw new LocalStorageException("Couldn't query dirty records"); + + long deleted[] = new long[cursor.getCount()]; + for (int idx = 0; cursor.moveToNext(); idx++) + deleted[idx] = cursor.getLong(0); + return deleted; + } catch(RemoteException ex) { + throw new LocalStorageException(ex); + } + } + + /** + * Finds a specific resource by ID. + * @param localID ID of the resource + * @param populate true: populates all data fields (for instance, contact or event details); + * false: only remote file name and ETag are populated + * @return resource with either ID/remote file/name/ETag or all fields populated + * @throws RecordNotFoundException when the resource couldn't be found + * @throws LocalStorageException when the content provider couldn't be queried + */ + public T findById(long localID, boolean populate) throws LocalStorageException { + try { + @Cleanup Cursor cursor = providerClient.query(ContentUris.withAppendedId(entriesURI(), localID), + new String[] { entryColumnRemoteName(), entryColumnETag() }, null, null, null); + if (cursor != null && cursor.moveToNext()) { + T resource = newResource(localID, cursor.getString(0), cursor.getString(1)); + if (populate) + populate(resource); + return resource; + } else + throw new RecordNotFoundException(); + } catch(RemoteException ex) { + throw new LocalStorageException(ex); + } + } + + /** + * Finds a specific resource by remote file name. + * @param localID remote file name of the resource + * @param populate true: populates all data fields (for instance, contact or event details); + * false: only remote file name and ETag are populated + * @return resource with either ID/remote file/name/ETag or all fields populated + * @throws RecordNotFoundException when the resource couldn't be found + * @throws LocalStorageException when the content provider couldn't be queried + */ + public T findByRemoteName(String remoteName, boolean populate) throws LocalStorageException { + try { + @Cleanup Cursor cursor = providerClient.query(entriesURI(), + new String[] { entryColumnID(), entryColumnRemoteName(), entryColumnETag() }, + entryColumnRemoteName() + "=?", new String[] { remoteName }, null); + if (cursor != null && cursor.moveToNext()) { + T resource = newResource(cursor.getLong(0), cursor.getString(1), cursor.getString(2)); + if (populate) + populate(resource); + return resource; + } else + throw new RecordNotFoundException(); + } catch(RemoteException ex) { + throw new LocalStorageException(ex); + } + } + + /** populates all data fields from the content provider */ + public abstract void populate(Resource record) throws LocalStorageException; + + + // create/update/delete + + /** + * Creates a new resource object in memory. No content provider operations involved. + * @param localID the ID of the resource + * @param resourceName the (remote) file name of the resource + * @param ETag of the resource + * @return the new resource object */ + abstract public T newResource(long localID, String resourceName, String eTag); + + /** Enqueues adding the resource (including all data) to the local collection. Requires commit(). */ + public void add(Resource resource) { + int idx = pendingOperations.size(); + pendingOperations.add( + buildEntry(ContentProviderOperation.newInsert(entriesURI()), resource) + .withYieldAllowed(true) + .build()); + + addDataRows(resource, -1, idx); + } + + /** Enqueues updating an existing resource in the local collection. The resource will be found by + * the remote file name and all data will be updated. Requires commit(). */ + public void updateByRemoteName(Resource remoteResource) throws LocalStorageException { + T localResource = findByRemoteName(remoteResource.getName(), false); + pendingOperations.add( + buildEntry(ContentProviderOperation.newUpdate(ContentUris.withAppendedId(entriesURI(), localResource.getLocalID())), remoteResource) + .withValue(entryColumnETag(), remoteResource.getETag()) + .withYieldAllowed(true) + .build()); + + removeDataRows(localResource); + addDataRows(remoteResource, localResource.getLocalID(), -1); + } + + /** Enqueues deleting a resource from the local collection. Requires commit(). */ + public void delete(Resource resource) { + pendingOperations.add(ContentProviderOperation + .newDelete(ContentUris.withAppendedId(entriesURI(), resource.getLocalID())) + .withYieldAllowed(true) + .build()); + } + + /** + * Enqueues deleting all resources except the give ones from the local collection. Requires commit(). + * @param remoteResources resources with these remote file names will be kept + */ + public abstract void deleteAllExceptRemoteNames(Resource[] remoteResources); + + /** Updates the locally-known ETag of a resource. */ + public void updateETag(Resource res, String eTag) throws LocalStorageException { + Log.d(TAG, "Setting ETag of local resource " + res + " to " + eTag); + + ContentValues values = new ContentValues(1); + values.put(entryColumnETag(), eTag); + try { + providerClient.update(ContentUris.withAppendedId(entriesURI(), res.getLocalID()), values, null, new String[] {}); + } catch (RemoteException e) { + throw new LocalStorageException(e); + } + } + + /** Enqueues removing the dirty flag from a locally-stored resource. Requires commit(). */ + public void clearDirty(Resource resource) { + pendingOperations.add(ContentProviderOperation + .newUpdate(ContentUris.withAppendedId(entriesURI(), resource.getLocalID())) + .withValue(entryColumnDirty(), 0) + .build()); + } + + /** Commits enqueued operations to the content provider (for batch operations). */ + public void commit() throws LocalStorageException { + if (!pendingOperations.isEmpty()) + try { + Log.d(TAG, "Committing " + pendingOperations.size() + " operations"); + providerClient.applyBatch(pendingOperations); + pendingOperations.clear(); + } catch (RemoteException ex) { + throw new LocalStorageException(ex); + } catch(OperationApplicationException ex) { + throw new LocalStorageException(ex); + } + } + + + // helpers + + protected void queueOperation(Builder builder) { + if (builder != null) + pendingOperations.add(builder.build()); + } + + /** Appends account type, name and CALLER_IS_SYNCADAPTER to an Uri. */ + protected Uri syncAdapterURI(Uri baseURI) { + return baseURI.buildUpon() + .appendQueryParameter(entryColumnAccountType(), account.type) + .appendQueryParameter(entryColumnAccountName(), account.name) + .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") + .build(); + } + + protected Builder newDataInsertBuilder(Uri dataUri, String refFieldName, long raw_ref_id, Integer backrefIdx) { + Builder builder = ContentProviderOperation.newInsert(syncAdapterURI(dataUri)); + if (backrefIdx != -1) + return builder.withValueBackReference(refFieldName, backrefIdx); + else + return builder.withValue(refFieldName, raw_ref_id); + } + + + // content builders + + /** + * Builds the main entry (for instance, a ContactsContract.RawContacts row) from a resource. + * The entry is built for insertion to the location identified by entriesURI(). + * + * @param builder Builder to be extended by all resource data that can be stored without extra data rows. + */ + protected abstract Builder buildEntry(Builder builder, Resource resource); + + /** Enqueues adding extra data rows of the resource to the local collection. */ + protected abstract void addDataRows(Resource resource, long localID, int backrefIdx); + + /** Enqueues removing all extra data rows of the resource from the local collection. */ + protected abstract void removeDataRows(Resource resource); +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/LocalStorageException.java b/app/src/main/java/at/bitfire/davdroid/resource/LocalStorageException.java new file mode 100644 index 00000000..d33ad46a --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/LocalStorageException.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.resource; + +public class LocalStorageException extends Exception { + private static final long serialVersionUID = -7787658815291629529L; + + private static final String detailMessage = "Couldn't access local content provider"; + + + public LocalStorageException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + public LocalStorageException(String detailMessage) { + super(detailMessage); + } + + public LocalStorageException(Throwable throwable) { + super(detailMessage, throwable); + } + + public LocalStorageException() { + super(detailMessage); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/RecordNotFoundException.java b/app/src/main/java/at/bitfire/davdroid/resource/RecordNotFoundException.java new file mode 100644 index 00000000..98daa77c --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/RecordNotFoundException.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.resource; + +/** + * Thrown when a local record (for instance, Contact with ID 12345) should be read + * but could not be found. + */ +public class RecordNotFoundException extends LocalStorageException { + private static final long serialVersionUID = 4961024282198632578L; + + private static final String detailMessage = "Record not found in local content provider"; + + + RecordNotFoundException(Throwable ex) { + super(detailMessage, ex); + } + + RecordNotFoundException() { + super(detailMessage); + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/RemoteCollection.java b/app/src/main/java/at/bitfire/davdroid/resource/RemoteCollection.java new file mode 100644 index 00000000..5d733e15 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/RemoteCollection.java @@ -0,0 +1,180 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.resource; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.util.LinkedList; +import java.util.List; + +import lombok.Cleanup; +import lombok.Getter; +import net.fortuna.ical4j.model.ValidationException; +import android.util.Log; +import at.bitfire.davdroid.URIUtils; +import at.bitfire.davdroid.webdav.DavException; +import at.bitfire.davdroid.webdav.DavMultiget; +import at.bitfire.davdroid.webdav.DavNoContentException; +import at.bitfire.davdroid.webdav.HttpException; +import at.bitfire.davdroid.webdav.HttpPropfind; +import at.bitfire.davdroid.webdav.WebDavResource; +import at.bitfire.davdroid.webdav.WebDavResource.PutMode; +import org.apache.http.impl.client.CloseableHttpClient; +import ezvcard.io.text.VCardParseException; + +/** + * Represents a remotely stored synchronizable collection (collection as in + * WebDAV terminology). + * + * @param Subtype of Resource that can be stored in the collection + */ +public abstract class RemoteCollection { + private static final String TAG = "davdroid.RemoteCollection"; + + CloseableHttpClient httpClient; + @Getter WebDavResource collection; + + abstract protected String memberContentType(); + abstract protected DavMultiget.Type multiGetType(); + abstract protected T newResourceSkeleton(String name, String ETag); + + public RemoteCollection(CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException { + this.httpClient = httpClient; + + collection = new WebDavResource(httpClient, URIUtils.parseURI(baseURL), user, password, preemptiveAuth); + } + + + /* collection operations */ + + public String getCTag() throws URISyntaxException, IOException, HttpException { + try { + if (collection.getCTag() == null && collection.getMembers() == null) // not already fetched + collection.propfind(HttpPropfind.Mode.COLLECTION_CTAG); + } catch (DavException e) { + return null; + } + return collection.getCTag(); + } + + public Resource[] getMemberETags() throws URISyntaxException, IOException, DavException, HttpException { + collection.propfind(HttpPropfind.Mode.MEMBERS_ETAG); + + List resources = new LinkedList(); + if (collection.getMembers() != null) { + for (WebDavResource member : collection.getMembers()) + resources.add(newResourceSkeleton(member.getName(), member.getETag())); + } + return resources.toArray(new Resource[0]); + } + + @SuppressWarnings("unchecked") + public Resource[] multiGet(Resource[] resources) throws URISyntaxException, IOException, DavException, HttpException { + try { + if (resources.length == 1) + return (T[]) new Resource[] { get(resources[0]) }; + + Log.i(TAG, "Multi-getting " + resources.length + " remote resource(s)"); + + LinkedList names = new LinkedList(); + for (Resource resource : resources) + names.add(resource.getName()); + + LinkedList foundResources = new LinkedList(); + collection.multiGet(multiGetType(), names.toArray(new String[0])); + if (collection.getMembers() == null) + throw new DavNoContentException(); + + for (WebDavResource member : collection.getMembers()) { + T resource = newResourceSkeleton(member.getName(), member.getETag()); + try { + if (member.getContent() != null) { + @Cleanup InputStream is = new ByteArrayInputStream(member.getContent()); + resource.parseEntity(is); + foundResources.add(resource); + } else + Log.e(TAG, "Ignoring entity without content"); + } catch (InvalidResourceException e) { + Log.e(TAG, "Ignoring unparseable entity in multi-response", e); + } + } + + return foundResources.toArray(new Resource[0]); + } catch (InvalidResourceException e) { + Log.e(TAG, "Couldn't parse entity from GET", e); + } + + return new Resource[0]; + } + + + /* internal member operations */ + + public Resource get(Resource resource) throws URISyntaxException, IOException, HttpException, DavException, InvalidResourceException { + WebDavResource member = new WebDavResource(collection, resource.getName()); + + if (resource instanceof Contact) + member.get(Contact.MIME_TYPE); + else if (resource instanceof Event) + member.get(Event.MIME_TYPE); + else { + Log.wtf(TAG, "Should fetch something, but neither contact nor calendar"); + throw new InvalidResourceException("Didn't now which MIME type to accept"); + } + + byte[] data = member.getContent(); + if (data == null) + throw new DavNoContentException(); + + @Cleanup InputStream is = new ByteArrayInputStream(data); + try { + resource.parseEntity(is); + } catch(VCardParseException e) { + throw new InvalidResourceException(e); + } + return resource; + } + + // returns ETag of the created resource, if returned by server + public String add(Resource res) throws URISyntaxException, IOException, HttpException, ValidationException { + WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag()); + member.setContentType(memberContentType()); + + @Cleanup ByteArrayOutputStream os = res.toEntity(); + String eTag = member.put(os.toByteArray(), PutMode.ADD_DONT_OVERWRITE); + + // after a successful upload, the collection has implicitely changed, too + collection.invalidateCTag(); + + return eTag; + } + + public void delete(Resource res) throws URISyntaxException, IOException, HttpException { + WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag()); + member.delete(); + + collection.invalidateCTag(); + } + + // returns ETag of the updated resource, if returned by server + public String update(Resource res) throws URISyntaxException, IOException, HttpException, ValidationException { + WebDavResource member = new WebDavResource(collection, res.getName(), res.getETag()); + member.setContentType(memberContentType()); + + @Cleanup ByteArrayOutputStream os = res.toEntity(); + String eTag = member.put(os.toByteArray(), PutMode.UPDATE_DONT_OVERWRITE); + + // after a successful upload, the collection has implicitely changed, too + collection.invalidateCTag(); + + return eTag; + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/Resource.java b/app/src/main/java/at/bitfire/davdroid/resource/Resource.java new file mode 100644 index 00000000..bda3e2e5 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/Resource.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.resource; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Represents a resource that can be contained in a LocalCollection or RemoteCollection + * for synchronization by WebDAV. + */ +@ToString +public abstract class Resource { + @Getter @Setter protected String name, ETag; + @Getter @Setter protected String uid; + @Getter protected long localID; + + + public Resource(String name, String ETag) { + this.name = name; + this.ETag = ETag; + } + + public Resource(long localID, String name, String ETag) { + this(name, ETag); + this.localID = localID; + } + + /** initializes UID and remote file name (required for first upload) */ + public abstract void initialize(); + + /** fills the resource data from an input stream (for instance, .vcf file for Contact) */ + public abstract void parseEntity(InputStream entity) throws IOException, InvalidResourceException; + /** writes the resource data to an output stream (for instance, .vcf file for Contact) */ + public abstract ByteArrayOutputStream toEntity() throws IOException; +} diff --git a/app/src/main/java/at/bitfire/davdroid/resource/ServerInfo.java b/app/src/main/java/at/bitfire/davdroid/resource/ServerInfo.java new file mode 100644 index 00000000..14f5b6f7 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/resource/ServerInfo.java @@ -0,0 +1,68 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.resource; + +import java.io.Serializable; +import java.net.URI; +import java.util.LinkedList; +import java.util.List; + +import ezvcard.VCardVersion; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor(suppressConstructorProperties=true) +@Data +public class ServerInfo implements Serializable { + private static final long serialVersionUID = 6744847358282980437L; + + enum Scheme { + HTTP, HTTPS, MAILTO + } + + final private URI baseURI; + final private String userName, password; + final boolean authPreemptive; + + private String errorMessage; + + private boolean calDAV = false, cardDAV = false; + private List + addressBooks = new LinkedList(), + calendars = new LinkedList(); + + + public boolean hasEnabledCalendars() { + for (ResourceInfo calendar : calendars) + if (calendar.enabled) + return true; + return false; + } + + + @RequiredArgsConstructor(suppressConstructorProperties=true) + @Data + public static class ResourceInfo implements Serializable { + private static final long serialVersionUID = -5516934508229552112L; + + public enum Type { + ADDRESS_BOOK, + CALENDAR + } + + boolean enabled = false; + + final Type type; + final boolean readOnly; + final String URL, title, description, color; + + VCardVersion vCardVersion; + + String timezone; + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountAuthenticatorService.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountAuthenticatorService.java new file mode 100644 index 00000000..f78cc4f6 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountAuthenticatorService.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.syncadapter; + +import android.accounts.AbstractAccountAuthenticator; +import android.accounts.Account; +import android.accounts.AccountAuthenticatorResponse; +import android.accounts.AccountManager; +import android.accounts.NetworkErrorException; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; + +public class AccountAuthenticatorService extends Service { + private static AccountAuthenticator accountAuthenticator; + + private AccountAuthenticator getAuthenticator() { + if (accountAuthenticator != null) + return accountAuthenticator; + return accountAuthenticator = new AccountAuthenticator(this); + } + + @Override + public IBinder onBind(Intent intent) { + if (intent.getAction().equals(android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT)) + return getAuthenticator().getIBinder(); + return null; + } + + + private static class AccountAuthenticator extends AbstractAccountAuthenticator { + Context context; + + public AccountAuthenticator(Context context) { + super(context); + this.context = context; + } + + @Override + public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, + String[] requiredFeatures, Bundle options) throws NetworkErrorException { + Intent intent = new Intent(context, AddAccountActivity.class); + intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); + Bundle bundle = new Bundle(); + bundle.putParcelable(AccountManager.KEY_INTENT, intent); + return bundle; + } + + @Override + public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException { + return null; + } + + @Override + public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { + return null; + } + + @Override + public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { + return null; + } + + @Override + public String getAuthTokenLabel(String authTokenType) { + return null; + } + + @Override + public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException { + return null; + } + + @Override + public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { + return null; + } + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountDetailsFragment.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountDetailsFragment.java new file mode 100644 index 00000000..37ab4734 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountDetailsFragment.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.syncadapter; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.Fragment; +import android.content.ContentResolver; +import android.os.Bundle; +import android.provider.CalendarContract; +import android.provider.ContactsContract; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; +import at.bitfire.davdroid.Constants; +import at.bitfire.davdroid.R; +import at.bitfire.davdroid.resource.LocalCalendar; +import at.bitfire.davdroid.resource.LocalStorageException; +import at.bitfire.davdroid.resource.ServerInfo; + +public class AccountDetailsFragment extends Fragment implements TextWatcher { + public static final String KEY_SERVER_INFO = "server_info"; + + ServerInfo serverInfo; + + EditText editAccountName; + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.account_details, container, false); + + serverInfo = (ServerInfo)getArguments().getSerializable(KEY_SERVER_INFO); + + editAccountName = (EditText)v.findViewById(R.id.account_name); + editAccountName.addTextChangedListener(this); + editAccountName.setText(serverInfo.getUserName()); + + TextView textAccountNameInfo = (TextView)v.findViewById(R.id.account_name_info); + if (!serverInfo.hasEnabledCalendars()) + textAccountNameInfo.setVisibility(View.GONE); + + setHasOptionsMenu(true); + return v; + } + + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.account_details, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.add_account: + addAccount(); + break; + default: + return false; + } + return true; + } + + + // actions + + void addAccount() { + ServerInfo serverInfo = (ServerInfo)getArguments().getSerializable(KEY_SERVER_INFO); + String accountName = editAccountName.getText().toString(); + + AccountManager accountManager = AccountManager.get(getActivity()); + Account account = new Account(accountName, Constants.ACCOUNT_TYPE); + Bundle userData = AccountSettings.createBundle(serverInfo); + + boolean syncContacts = false; + for (ServerInfo.ResourceInfo addressBook : serverInfo.getAddressBooks()) + if (addressBook.isEnabled()) { + ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1); + syncContacts = true; + continue; + } + if (syncContacts) { + ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1); + ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true); + } else + ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 0); + + if (accountManager.addAccountExplicitly(account, serverInfo.getPassword(), userData)) { + // account created, now create calendars + boolean syncCalendars = false; + for (ServerInfo.ResourceInfo calendar : serverInfo.getCalendars()) + if (calendar.isEnabled()) + try { + LocalCalendar.create(account, getActivity().getContentResolver(), calendar); + syncCalendars = true; + } catch (LocalStorageException e) { + Toast.makeText(getActivity(), "Couldn't create calendar(s): " + e.getMessage(), Toast.LENGTH_LONG).show(); + } + if (syncCalendars) { + ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1); + ContentResolver.setSyncAutomatically(account, CalendarContract.AUTHORITY, true); + } else + ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 0); + + getActivity().finish(); + } else + Toast.makeText(getActivity(), "Couldn't create account (account with this name already existing?)", Toast.LENGTH_LONG).show(); + } + + + // input validation + + @Override + public void onPrepareOptionsMenu(Menu menu) { + boolean ok = false; + ok = editAccountName.getText().length() > 0; + MenuItem item = menu.findItem(R.id.add_account); + item.setEnabled(ok); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + getActivity().invalidateOptionsMenu(); + } + + @Override + public void afterTextChanged(Editable s) { + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountSettings.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountSettings.java new file mode 100644 index 00000000..a58fe407 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/AccountSettings.java @@ -0,0 +1,183 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.syncadapter; + +import java.net.URI; +import java.net.URISyntaxException; + +import lombok.Cleanup; +import android.accounts.Account; +import android.accounts.AccountManager; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.CalendarContract; +import android.provider.CalendarContract.Calendars; +import android.util.Log; +import at.bitfire.davdroid.resource.ServerInfo; +import ezvcard.VCardVersion; + +public class AccountSettings { + private final static String TAG = "davdroid.AccountSettings"; + + private final static int CURRENT_VERSION = 1; + private final static String + KEY_SETTINGS_VERSION = "version", + + KEY_USERNAME = "user_name", + KEY_AUTH_PREEMPTIVE = "auth_preemptive", + + KEY_ADDRESSBOOK_URL = "addressbook_url", + KEY_ADDRESSBOOK_CTAG = "addressbook_ctag", + KEY_ADDRESSBOOK_VCARD_VERSION = "addressbook_vcard_version"; + + Context context; + AccountManager accountManager; + Account account; + + + public AccountSettings(Context context, Account account) { + this.context = context; + this.account = account; + + accountManager = AccountManager.get(context); + + synchronized(AccountSettings.class) { + int version = 0; + try { + version = Integer.parseInt(accountManager.getUserData(account, KEY_SETTINGS_VERSION)); + } catch(NumberFormatException e) { + } + if (version < CURRENT_VERSION) + update(version); + } + } + + + public static Bundle createBundle(ServerInfo serverInfo) { + Bundle bundle = new Bundle(); + bundle.putString(KEY_SETTINGS_VERSION, String.valueOf(CURRENT_VERSION)); + bundle.putString(KEY_USERNAME, serverInfo.getUserName()); + bundle.putString(KEY_AUTH_PREEMPTIVE, Boolean.toString(serverInfo.isAuthPreemptive())); + for (ServerInfo.ResourceInfo addressBook : serverInfo.getAddressBooks()) + if (addressBook.isEnabled()) { + bundle.putString(KEY_ADDRESSBOOK_URL, addressBook.getURL()); + bundle.putString(KEY_ADDRESSBOOK_VCARD_VERSION, addressBook.getVCardVersion().getVersion()); + continue; + } + return bundle; + } + + + // general settings + + public String getUserName() { + return accountManager.getUserData(account, KEY_USERNAME); + } + + public String getPassword() { + return accountManager.getPassword(account); + } + + public boolean getPreemptiveAuth() { + return Boolean.parseBoolean(accountManager.getUserData(account, KEY_AUTH_PREEMPTIVE)); + } + + + // address book (CardDAV) settings + + public String getAddressBookURL() { + return accountManager.getUserData(account, KEY_ADDRESSBOOK_URL); + } + + public String getAddressBookCTag() { + return accountManager.getUserData(account, KEY_ADDRESSBOOK_CTAG); + } + + public void setAddressBookCTag(String cTag) { + accountManager.setUserData(account, KEY_ADDRESSBOOK_CTAG, cTag); + } + + public VCardVersion getAddressBookVCardVersion() { + VCardVersion version = VCardVersion.V3_0; + String versionStr = accountManager.getUserData(account, KEY_ADDRESSBOOK_VCARD_VERSION); + if (versionStr != null) + version = VCardVersion.valueOfByStr(versionStr); + return version; + } + + + // update from previous account settings + + private void update(int fromVersion) { + Log.i(TAG, "Account settings must be updated from v" + fromVersion + " to v" + CURRENT_VERSION); + for (int toVersion = CURRENT_VERSION; toVersion > fromVersion; toVersion--) + update(fromVersion, toVersion); + } + + private void update(int fromVersion, int toVersion) { + Log.i(TAG, "Updating account settings from v" + fromVersion + " to " + toVersion); + try { + if (fromVersion == 0 && toVersion == 1) + update_0_1(); + else + Log.wtf(TAG, "Don't know how to update settings from v" + fromVersion + " to v" + toVersion); + } catch(Exception e) { + Log.e(TAG, "Couldn't update account settings (DAVdroid will probably crash)!", e); + } + } + + private void update_0_1() throws URISyntaxException { + String v0_principalURL = accountManager.getUserData(account, "principal_url"), + v0_addressBookPath = accountManager.getUserData(account, "addressbook_path"); + Log.d(TAG, "Old principal URL = " + v0_principalURL); + Log.d(TAG, "Old address book path = " + v0_addressBookPath); + + URI principalURI = new URI(v0_principalURL); + + // update address book + if (v0_addressBookPath != null) { + String addressBookURL = principalURI.resolve(v0_addressBookPath).toASCIIString(); + Log.d(TAG, "New address book URL = " + addressBookURL); + accountManager.setUserData(account, "addressbook_url", addressBookURL); + } + + // update calendars + ContentResolver resolver = context.getContentResolver(); + Uri calendars = Calendars.CONTENT_URI.buildUpon() + .appendQueryParameter(Calendars.ACCOUNT_NAME, account.name) + .appendQueryParameter(Calendars.ACCOUNT_TYPE, account.type) + .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true").build(); + @Cleanup Cursor cursor = resolver.query(calendars, new String[] { Calendars._ID, Calendars.NAME }, null, null, null); + while (cursor != null && cursor.moveToNext()) { + int id = cursor.getInt(0); + String v0_path = cursor.getString(1), + v1_url = principalURI.resolve(v0_path).toASCIIString(); + Log.d(TAG, "Updating calendar #" + id + " name: " + v0_path + " -> " + v1_url); + Uri calendar = ContentUris.appendId(Calendars.CONTENT_URI.buildUpon() + .appendQueryParameter(Calendars.ACCOUNT_NAME, account.name) + .appendQueryParameter(Calendars.ACCOUNT_TYPE, account.type) + .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true"), id).build(); + ContentValues newValues = new ContentValues(1); + newValues.put(Calendars.NAME, v1_url); + if (resolver.update(calendar, newValues, null, null) != 1) + Log.e(TAG, "Number of modified calendars != 1"); + } + + Log.d(TAG, "Cleaning old principal URL and address book path"); + accountManager.setUserData(account, "principal_url", null); + accountManager.setUserData(account, "addressbook_path", null); + + Log.d(TAG, "Updated settings successfully!"); + accountManager.setUserData(account, KEY_SETTINGS_VERSION, "1"); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/AddAccountActivity.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/AddAccountActivity.java new file mode 100644 index 00000000..f65f42f9 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/AddAccountActivity.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.syncadapter; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import at.bitfire.davdroid.Constants; +import at.bitfire.davdroid.R; + +public class AddAccountActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.add_account); + + if (savedInstanceState == null) { // first call + getFragmentManager().beginTransaction() + .add(R.id.fragment_container, new LoginTypeFragment(), "login_type") + .commit(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.add_account, menu); + return true; + } + + public void showHelp(MenuItem item) { + startActivityForResult(new Intent(Intent.ACTION_VIEW, Uri.parse(Constants.WEB_URL_HELP)), 0); + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java new file mode 100644 index 00000000..2746163b --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.syncadapter; + +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +import android.accounts.Account; +import android.app.Service; +import android.content.ContentProviderClient; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import at.bitfire.davdroid.resource.CalDavCalendar; +import at.bitfire.davdroid.resource.LocalCalendar; +import at.bitfire.davdroid.resource.LocalCollection; +import at.bitfire.davdroid.resource.RemoteCollection; + +public class CalendarsSyncAdapterService extends Service { + private static SyncAdapter syncAdapter; + + + @Override + public void onCreate() { + if (syncAdapter == null) + syncAdapter = new SyncAdapter(getApplicationContext()); + } + + @Override + public void onDestroy() { + syncAdapter.close(); + syncAdapter = null; + } + + @Override + public IBinder onBind(Intent intent) { + return syncAdapter.getSyncAdapterBinder(); + } + + + private static class SyncAdapter extends DavSyncAdapter { + private final static String TAG = "davdroid.CalendarsSyncAdapter"; + + + private SyncAdapter(Context context) { + super(context); + } + + @Override + protected Map, RemoteCollection> getSyncPairs(Account account, ContentProviderClient provider) { + AccountSettings settings = new AccountSettings(getContext(), account); + String userName = settings.getUserName(), + password = settings.getPassword(); + boolean preemptive = settings.getPreemptiveAuth(); + + try { + Map, RemoteCollection> map = new HashMap, RemoteCollection>(); + + for (LocalCalendar calendar : LocalCalendar.findAll(account, provider)) { + RemoteCollection dav = new CalDavCalendar(httpClient, calendar.getUrl(), userName, password, preemptive); + map.put(calendar, dav); + } + return map; + } catch (RemoteException ex) { + Log.e(TAG, "Couldn't find local calendars", ex); + } catch (URISyntaxException ex) { + Log.e(TAG, "Couldn't build calendar URI", ex); + } + + return null; + } + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java new file mode 100644 index 00000000..7e8f02a2 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.syncadapter; + +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +import android.accounts.Account; +import android.app.Service; +import android.content.ContentProviderClient; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.util.Log; +import at.bitfire.davdroid.resource.CardDavAddressBook; +import at.bitfire.davdroid.resource.LocalAddressBook; +import at.bitfire.davdroid.resource.LocalCollection; +import at.bitfire.davdroid.resource.RemoteCollection; + +public class ContactsSyncAdapterService extends Service { + private static ContactsSyncAdapter syncAdapter; + + + @Override + public void onCreate() { + if (syncAdapter == null) + syncAdapter = new ContactsSyncAdapter(getApplicationContext()); + } + + @Override + public void onDestroy() { + syncAdapter.close(); + syncAdapter = null; + } + + @Override + public IBinder onBind(Intent intent) { + return syncAdapter.getSyncAdapterBinder(); + } + + + private static class ContactsSyncAdapter extends DavSyncAdapter { + private final static String TAG = "davdroid.ContactsSyncAdapter"; + + + private ContactsSyncAdapter(Context context) { + super(context); + } + + @Override + protected Map, RemoteCollection> getSyncPairs(Account account, ContentProviderClient provider) { + AccountSettings settings = new AccountSettings(getContext(), account); + String userName = settings.getUserName(), + password = settings.getPassword(); + boolean preemptive = settings.getPreemptiveAuth(); + + String addressBookURL = settings.getAddressBookURL(); + if (addressBookURL == null) + return null; + + try { + LocalCollection database = new LocalAddressBook(account, provider, settings); + RemoteCollection dav = new CardDavAddressBook(httpClient, addressBookURL, userName, password, preemptive); + + Map, RemoteCollection> map = new HashMap, RemoteCollection>(); + map.put(database, dav); + + return map; + } catch (URISyntaxException ex) { + Log.e(TAG, "Couldn't build address book URI", ex); + } + + return null; + } + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/DavSyncAdapter.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/DavSyncAdapter.java new file mode 100644 index 00000000..00d045af --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/DavSyncAdapter.java @@ -0,0 +1,166 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.syncadapter; + +import java.io.Closeable; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Map; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import lombok.Getter; + +import org.apache.http.HttpStatus; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.content.AbstractThreadedSyncAdapter; +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SyncResult; +import android.os.AsyncTask; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.provider.Settings; +import android.util.Log; +import at.bitfire.davdroid.Constants; +import at.bitfire.davdroid.resource.LocalCollection; +import at.bitfire.davdroid.resource.LocalStorageException; +import at.bitfire.davdroid.resource.RemoteCollection; +import at.bitfire.davdroid.webdav.DavException; +import at.bitfire.davdroid.webdav.DavHttpClient; +import at.bitfire.davdroid.webdav.HttpException; +import org.apache.http.impl.client.CloseableHttpClient; + +public abstract class DavSyncAdapter extends AbstractThreadedSyncAdapter implements Closeable { + private final static String TAG = "davdroid.DavSyncAdapter"; + + @Getter private static String androidID; + + protected AccountManager accountManager; + + /* We use one static httpClient for + * - all sync adapters (CalendarsSyncAdapter, ContactsSyncAdapter) + * - and all threads (= accounts) of each sync adapter + * so that HttpClient's threaded pool management can do its best. + */ + protected static CloseableHttpClient httpClient; + + /* One static read/write lock pair for the static httpClient: + * Use the READ lock when httpClient will only be called (to prevent it from being unset while being used). + * Use the WRITE lock when httpClient will be modified (set/unset). */ + private final static ReentrantReadWriteLock httpClientLock = new ReentrantReadWriteLock(); + + + public DavSyncAdapter(Context context) { + super(context, true); + + synchronized(this) { + if (androidID == null) + androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); + } + + accountManager = AccountManager.get(context); + } + + @Override + public void close() { + Log.d(TAG, "Closing httpClient"); + + // may be called from a GUI thread, so we need an AsyncTask + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + try { + httpClientLock.writeLock().lock(); + if (httpClient != null) { + httpClient.close(); + httpClient = null; + } + httpClientLock.writeLock().unlock(); + } catch (IOException e) { + Log.w(TAG, "Couldn't close HTTP client", e); + } + return null; + } + }.execute(); + } + + protected abstract Map, RemoteCollection> getSyncPairs(Account account, ContentProviderClient provider); + + + @Override + public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { + Log.i(TAG, "Performing sync for authority " + authority); + + // set class loader for iCal4j ResourceLoader + Thread.currentThread().setContextClassLoader(getContext().getClassLoader()); + + // create httpClient, if necessary + httpClientLock.writeLock().lock(); + if (httpClient == null) { + Log.d(TAG, "Creating new DavHttpClient"); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getContext()); + httpClient = DavHttpClient.create( + settings.getBoolean(Constants.SETTING_DISABLE_COMPRESSION, false), + settings.getBoolean(Constants.SETTING_NETWORK_LOGGING, false) + ); + } + + // prevent httpClient shutdown until we're ready by holding a read lock + // acquiring read lock before releasing write lock will downgrade the write lock to a read lock + httpClientLock.readLock().lock(); + httpClientLock.writeLock().unlock(); + + // TODO use VCard 4.0 if possible + AccountSettings accountSettings = new AccountSettings(getContext(), account); + Log.d(TAG, "Server supports VCard version " + accountSettings.getAddressBookVCardVersion()); + + try { + // get local <-> remote collection pairs + Map, RemoteCollection> syncCollections = getSyncPairs(account, provider); + if (syncCollections == null) + Log.i(TAG, "Nothing to synchronize"); + else + try { + for (Map.Entry, RemoteCollection> entry : syncCollections.entrySet()) + new SyncManager(entry.getKey(), entry.getValue()).synchronize(extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL), syncResult); + } catch (DavException ex) { + syncResult.stats.numParseExceptions++; + Log.e(TAG, "Invalid DAV response", ex); + } catch (HttpException ex) { + if (ex.getCode() == HttpStatus.SC_UNAUTHORIZED) { + Log.e(TAG, "HTTP Unauthorized " + ex.getCode(), ex); + syncResult.stats.numAuthExceptions++; + } else if (ex.isClientError()) { + Log.e(TAG, "Hard HTTP error " + ex.getCode(), ex); + syncResult.stats.numParseExceptions++; + } else { + Log.w(TAG, "Soft HTTP error " + ex.getCode() + " (Android will try again later)", ex); + syncResult.stats.numIoExceptions++; + } + } catch (LocalStorageException ex) { + syncResult.databaseError = true; + Log.e(TAG, "Local storage (content provider) exception", ex); + } catch (IOException ex) { + syncResult.stats.numIoExceptions++; + Log.e(TAG, "I/O error (Android will try again later)", ex); + } catch (URISyntaxException ex) { + Log.e(TAG, "Invalid URI (file name) syntax", ex); + } + } finally { + // allow httpClient shutdown + httpClientLock.readLock().unlock(); + } + + Log.i(TAG, "Sync complete for " + authority); + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/GeneralSettingsActivity.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/GeneralSettingsActivity.java new file mode 100644 index 00000000..9135b428 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/GeneralSettingsActivity.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.syncadapter; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.preference.PreferenceFragment; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import at.bitfire.davdroid.R; + +public class GeneralSettingsActivity extends Activity { + final static String URL_REPORT_ISSUE = "https://github.com/bitfireAT/davdroid/blob/master/CONTRIBUTING.md#reporting-issues"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getFragmentManager().beginTransaction() + .replace(android.R.id.content, new GeneralSettingsFragment()) + .commit(); + } + + public void reportIssue(MenuItem item) { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(URL_REPORT_ISSUE))); + } + + + public static class GeneralSettingsFragment extends PreferenceFragment { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getPreferenceManager().setSharedPreferencesMode(Context.MODE_MULTI_PROCESS); + addPreferencesFromResource(R.xml.general_settings); + + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.debug_settings, menu); + } + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/LoginEmailFragment.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/LoginEmailFragment.java new file mode 100644 index 00000000..f0879941 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/LoginEmailFragment.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.syncadapter; + +import java.net.URI; +import java.net.URISyntaxException; + +import android.app.DialogFragment; +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import at.bitfire.davdroid.R; + +public class LoginEmailFragment extends Fragment implements TextWatcher { + + protected EditText editEmail, editPassword; + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.login_email, container, false); + + editEmail = (EditText)v.findViewById(R.id.email_address); + editEmail.addTextChangedListener(this); + editPassword = (EditText)v.findViewById(R.id.password); + editPassword.addTextChangedListener(this); + + setHasOptionsMenu(true); + return v; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.only_next, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.next: + FragmentTransaction ft = getFragmentManager().beginTransaction(); + + Bundle args = new Bundle(); + String email = editEmail.getText().toString(); + args.putString(QueryServerDialogFragment.EXTRA_BASE_URI, "mailto:" + email); + args.putString(QueryServerDialogFragment.EXTRA_USER_NAME, email); + args.putString(QueryServerDialogFragment.EXTRA_PASSWORD, editPassword.getText().toString()); + args.putBoolean(QueryServerDialogFragment.EXTRA_AUTH_PREEMPTIVE, true); + + DialogFragment dialog = new QueryServerDialogFragment(); + dialog.setArguments(args); + dialog.show(ft, QueryServerDialogFragment.class.getName()); + break; + default: + return false; + } + return true; + } + + + // input validation + + @Override + public void onPrepareOptionsMenu(Menu menu) { + boolean emailOk = false, + passwordOk = editPassword.getText().length() > 0; + + String email = editEmail.getText().toString(); + try { + URI uri = new URI("mailto:" + email); + if (uri.isOpaque()) { + int pos = email.lastIndexOf("@"); + if (pos != -1) + emailOk = !email.substring(pos+1).isEmpty(); + } + } catch (URISyntaxException e) { + // invalid mailto: URI + } + + MenuItem item = menu.findItem(R.id.next); + item.setEnabled(emailOk && passwordOk); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + getActivity().invalidateOptionsMenu(); + } + + @Override + public void afterTextChanged(Editable s) { + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/LoginTypeFragment.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/LoginTypeFragment.java new file mode 100644 index 00000000..8507bcc6 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/LoginTypeFragment.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.syncadapter; + +import android.app.Fragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RadioButton; +import at.bitfire.davdroid.R; + +public class LoginTypeFragment extends Fragment { + + protected RadioButton btnTypeEmail, btnTypeURL; + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.login_type, container, false); + + btnTypeEmail = (RadioButton)v.findViewById(R.id.login_type_email); + btnTypeURL = (RadioButton)v.findViewById(R.id.login_type_url); + + setHasOptionsMenu(true); + + return v; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.only_next, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.next: + Fragment loginFragment = btnTypeEmail.isChecked() ? new LoginEmailFragment() : new LoginURLFragment(); + getFragmentManager().beginTransaction() + .replace(R.id.fragment_container, loginFragment) + .addToBackStack(null) + .commitAllowingStateLoss(); + return true; + default: + return false; + } + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/LoginURLFragment.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/LoginURLFragment.java new file mode 100644 index 00000000..5703fa93 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/LoginURLFragment.java @@ -0,0 +1,153 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.syncadapter; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.apache.commons.lang.StringUtils; + +import android.app.DialogFragment; +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.TextView; +import at.bitfire.davdroid.R; + +public class LoginURLFragment extends Fragment implements TextWatcher { + + protected Spinner spnrScheme; + protected TextView textHttpWarning; + protected EditText editBaseURI, editUserName, editPassword; + protected CheckBox checkboxPreemptive; + protected Button btnNext; + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.login_url, container, false); + + // protocol selection spinner + textHttpWarning = (TextView)v.findViewById(R.id.http_warning); + + spnrScheme = (Spinner)v.findViewById(R.id.login_scheme); + spnrScheme.setOnItemSelectedListener(new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + String scheme = parent.getAdapter().getItem(position).toString(); + textHttpWarning.setVisibility(scheme.equals("https://") ? View.GONE : View.VISIBLE); + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }); + spnrScheme.setSelection(1); // HTTPS + + // other input fields + editBaseURI = (EditText)v.findViewById(R.id.login_host_path); + editBaseURI.addTextChangedListener(this); + + editUserName = (EditText)v.findViewById(R.id.userName); + editUserName.addTextChangedListener(this); + + editPassword = (EditText)v.findViewById(R.id.password); + editPassword.addTextChangedListener(this); + + checkboxPreemptive = (CheckBox) v.findViewById(R.id.auth_preemptive); + + // hook into action bar + setHasOptionsMenu(true); + + return v; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.only_next, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.next: + FragmentTransaction ft = getFragmentManager().beginTransaction(); + + Bundle args = new Bundle(); + try { + args.putString(QueryServerDialogFragment.EXTRA_BASE_URI, getBaseURI().toString()); + } catch (URISyntaxException e) { + } + args.putString(QueryServerDialogFragment.EXTRA_USER_NAME, editUserName.getText().toString()); + args.putString(QueryServerDialogFragment.EXTRA_PASSWORD, editPassword.getText().toString()); + args.putBoolean(QueryServerDialogFragment.EXTRA_AUTH_PREEMPTIVE, checkboxPreemptive.isChecked()); + + DialogFragment dialog = new QueryServerDialogFragment(); + dialog.setArguments(args); + dialog.show(ft, QueryServerDialogFragment.class.getName()); + break; + default: + return false; + } + return true; + } + + + private URI getBaseURI() throws URISyntaxException { + String scheme = spnrScheme.getSelectedItem().toString(), + host_path = editBaseURI.getText().toString(); + return new URI(scheme + host_path); + } + + + // input validation + + @Override + public void onPrepareOptionsMenu(Menu menu) { + boolean usernameOk = editUserName.getText().length() > 0, + passwordOk = editPassword.getText().length() > 0, + urlOk = false; + + // check host name + try { + if (!StringUtils.isBlank(getBaseURI().getHost())) + urlOk = true; + } catch (Exception e) { + } + + MenuItem item = menu.findItem(R.id.next); + item.setEnabled(usernameOk && passwordOk && urlOk); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + getActivity().invalidateOptionsMenu(); + } + + @Override + public void afterTextChanged(Editable s) { + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/QueryServerDialogFragment.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/QueryServerDialogFragment.java new file mode 100644 index 00000000..10f64d83 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/QueryServerDialogFragment.java @@ -0,0 +1,129 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.syncadapter; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +import lombok.Cleanup; +import android.app.DialogFragment; +import android.app.LoaderManager.LoaderCallbacks; +import android.content.AsyncTaskLoader; +import android.content.Context; +import android.content.Loader; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.Toast; +import at.bitfire.davdroid.R; +import at.bitfire.davdroid.resource.DavResourceFinder; +import at.bitfire.davdroid.resource.ServerInfo; +import at.bitfire.davdroid.webdav.DavException; +import org.apache.http.HttpException; + +public class QueryServerDialogFragment extends DialogFragment implements LoaderCallbacks { + private static final String TAG = "davdroid.QueryServerDialogFragment"; + public static final String + EXTRA_BASE_URI = "base_uri", + EXTRA_USER_NAME = "user_name", + EXTRA_PASSWORD = "password", + EXTRA_AUTH_PREEMPTIVE = "auth_preemptive"; + + ProgressBar progressBar; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setStyle(DialogFragment.STYLE_NO_TITLE, android.R.style.Theme_Holo_Light_Dialog); + setCancelable(false); + + Loader loader = getLoaderManager().initLoader(0, getArguments(), this); + if (savedInstanceState == null) // http://code.google.com/p/android/issues/detail?id=14944 + loader.forceLoad(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = inflater.inflate(R.layout.query_server, container, false); + return v; + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + Log.i(TAG, "onCreateLoader"); + return new ServerInfoLoader(getActivity(), args); + } + + @Override + public void onLoadFinished(Loader loader, ServerInfo serverInfo) { + if (serverInfo.getErrorMessage() != null) + Toast.makeText(getActivity(), serverInfo.getErrorMessage(), Toast.LENGTH_LONG).show(); + else { + SelectCollectionsFragment selectCollections = new SelectCollectionsFragment(); + Bundle arguments = new Bundle(); + arguments.putSerializable(SelectCollectionsFragment.KEY_SERVER_INFO, serverInfo); + selectCollections.setArguments(arguments); + + getFragmentManager().beginTransaction() + .replace(R.id.fragment_container, selectCollections) + .addToBackStack(null) + .commitAllowingStateLoss(); + } + + getDialog().dismiss(); + } + + @Override + public void onLoaderReset(Loader arg0) { + } + + + static class ServerInfoLoader extends AsyncTaskLoader { + private static final String TAG = "davdroid.ServerInfoLoader"; + final Bundle args; + final Context context; + + public ServerInfoLoader(Context context, Bundle args) { + super(context); + this.context = context; + this.args = args; + } + + @Override + public ServerInfo loadInBackground() { + ServerInfo serverInfo = new ServerInfo( + URI.create(args.getString(EXTRA_BASE_URI)), + args.getString(EXTRA_USER_NAME), + args.getString(EXTRA_PASSWORD), + args.getBoolean(EXTRA_AUTH_PREEMPTIVE) + ); + + try { + @Cleanup DavResourceFinder finder = new DavResourceFinder(context); + finder.findResources(serverInfo); + } catch (URISyntaxException e) { + serverInfo.setErrorMessage(getContext().getString(R.string.exception_uri_syntax, e.getMessage())); + } catch (IOException e) { + serverInfo.setErrorMessage(getContext().getString(R.string.exception_io, e.getLocalizedMessage())); + } catch (HttpException e) { + Log.e(TAG, "HTTP error while querying server info", e); + serverInfo.setErrorMessage(getContext().getString(R.string.exception_http, e.getLocalizedMessage())); + } catch (DavException e) { + Log.e(TAG, "DAV error while querying server info", e); + serverInfo.setErrorMessage(getContext().getString(R.string.exception_incapable_resource, e.getLocalizedMessage())); + } + + return serverInfo; + } + + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SelectCollectionsAdapter.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/SelectCollectionsAdapter.java new file mode 100644 index 00000000..95c871ed --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SelectCollectionsAdapter.java @@ -0,0 +1,161 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.syncadapter; + +import lombok.Getter; +import android.annotation.SuppressLint; +import android.content.Context; +import android.text.Html; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.CheckedTextView; +import android.widget.ListAdapter; +import at.bitfire.davdroid.R; +import at.bitfire.davdroid.resource.ServerInfo; +import at.bitfire.davdroid.resource.ServerInfo.ResourceInfo.Type; + +public class SelectCollectionsAdapter extends BaseAdapter implements ListAdapter { + final static int TYPE_ADDRESS_BOOKS_HEADING = 0, + TYPE_ADDRESS_BOOKS_ROW = 1, + TYPE_CALENDARS_HEADING = 2, + TYPE_CALENDARS_ROW = 3; + + protected Context context; + protected ServerInfo serverInfo; + @Getter protected int nAddressBooks, nCalendars; + + + public SelectCollectionsAdapter(Context context, ServerInfo serverInfo) { + this.context = context; + + this.serverInfo = serverInfo; + nAddressBooks = (serverInfo.getAddressBooks() == null) ? 0 : serverInfo.getAddressBooks().size(); + nCalendars = (serverInfo.getCalendars() == null) ? 0 : serverInfo.getCalendars().size(); + } + + + // item data + + @Override + public int getCount() { + return nAddressBooks + nCalendars + 2; + } + + @Override + public Object getItem(int position) { + if (position > 0 && position <= nAddressBooks) + return serverInfo.getAddressBooks().get(position - 1); + else if (position > nAddressBooks + 1) + return serverInfo.getCalendars().get(position - nAddressBooks - 2); + return null; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public long getItemId(int position) { + return position; + } + + + // item views + + @Override + public int getViewTypeCount() { + return 4; + } + + @Override + public int getItemViewType(int position) { + if (position == 0) + return TYPE_ADDRESS_BOOKS_HEADING; + else if (position <= nAddressBooks) + return TYPE_ADDRESS_BOOKS_ROW; + else if (position == nAddressBooks + 1) + return TYPE_CALENDARS_HEADING; + else if (position <= nAddressBooks + nCalendars + 1) + return TYPE_CALENDARS_ROW; + else + return IGNORE_ITEM_VIEW_TYPE; + } + + @Override + @SuppressLint("InflateParams") + public View getView(int position, View convertView, ViewGroup parent) { + View v = convertView; + + // step 1: get view (either by creating or recycling) + if (v == null) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + switch (getItemViewType(position)) { + case TYPE_ADDRESS_BOOKS_HEADING: + v = inflater.inflate(R.layout.address_books_heading, parent, false); + break; + case TYPE_ADDRESS_BOOKS_ROW: + v = inflater.inflate(android.R.layout.simple_list_item_single_choice, null); + v.setPadding(0, 8, 0, 8); + break; + case TYPE_CALENDARS_HEADING: + v = inflater.inflate(R.layout.calendars_heading, parent, false); + break; + case TYPE_CALENDARS_ROW: + v = inflater.inflate(android.R.layout.simple_list_item_multiple_choice, null); + v.setPadding(0, 8, 0, 8); + } + } + + // step 2: fill view with content + switch (getItemViewType(position)) { + case TYPE_ADDRESS_BOOKS_ROW: + setContent((CheckedTextView)v, R.drawable.addressbook, (ServerInfo.ResourceInfo)getItem(position)); + break; + case TYPE_CALENDARS_ROW: + setContent((CheckedTextView)v, R.drawable.calendar, (ServerInfo.ResourceInfo)getItem(position)); + } + + return v; + } + + protected void setContent(CheckedTextView view, int collectionIcon, ServerInfo.ResourceInfo info) { + // set layout and icons + view.setCompoundDrawablesWithIntrinsicBounds(collectionIcon, 0, info.isReadOnly() ? R.drawable.ic_read_only : 0, 0); + view.setCompoundDrawablePadding(10); + + // set text + String title = info.getTitle(); + if (title == null) // unnamed collection + title = context.getString((info.getType() == Type.ADDRESS_BOOK) ? + R.string.setup_address_book : R.string.setup_calendar); + title = "" + title + ""; + if (info.isReadOnly()) + title = title + " (" + context.getString(R.string.setup_read_only) + ")"; + + String description = info.getDescription(); + if (description == null) + description = info.getURL(); + + // FIXME escape HTML + view.setText(Html.fromHtml(title + "
" + description)); + } + + @Override + public boolean areAllItemsEnabled() { + return false; + } + + @Override + public boolean isEnabled(int position) { + int type = getItemViewType(position); + return (type == TYPE_ADDRESS_BOOKS_ROW || type == TYPE_CALENDARS_ROW); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SelectCollectionsFragment.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/SelectCollectionsFragment.java new file mode 100644 index 00000000..c7a044b2 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SelectCollectionsFragment.java @@ -0,0 +1,127 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.syncadapter; + +import android.app.ListFragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListAdapter; +import android.widget.ListView; +import at.bitfire.davdroid.R; +import at.bitfire.davdroid.resource.ServerInfo; + +public class SelectCollectionsFragment extends ListFragment { + public static final String KEY_SERVER_INFO = "server_info"; + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = super.onCreateView(inflater, container, savedInstanceState); + setHasOptionsMenu(true); + return v; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + setListAdapter(null); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + final ListView listView = getListView(); + listView.setPadding(20, 30, 20, 30); + + View header = getActivity().getLayoutInflater().inflate(R.layout.select_collections_header, getListView(), false); + listView.addHeaderView(header, getListView(), false); + + final ServerInfo serverInfo = (ServerInfo)getArguments().getSerializable(KEY_SERVER_INFO); + final SelectCollectionsAdapter adapter = new SelectCollectionsAdapter(view.getContext(), serverInfo); + setListAdapter(adapter); + + listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + listView.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + int itemPosition = position - 1; // one list header view at pos. 0 + if (adapter.getItemViewType(itemPosition) == SelectCollectionsAdapter.TYPE_ADDRESS_BOOKS_ROW) { + // unselect all other address books + for (int pos = 1; pos <= adapter.getNAddressBooks(); pos++) + if (pos != itemPosition) + listView.setItemChecked(pos + 1, false); + } + + getActivity().invalidateOptionsMenu(); + } + }); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.only_next, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.next: + ServerInfo serverInfo = (ServerInfo)getArguments().getSerializable(KEY_SERVER_INFO); + + // synchronize only selected collections + for (ServerInfo.ResourceInfo addressBook : serverInfo.getAddressBooks()) + addressBook.setEnabled(false); + for (ServerInfo.ResourceInfo calendar : serverInfo.getCalendars()) + calendar.setEnabled(false); + + ListAdapter adapter = getListView().getAdapter(); + for (long id : getListView().getCheckedItemIds()) { + int position = (int)id + 1; // +1 because header view is inserted at pos. 0 + ServerInfo.ResourceInfo info = (ServerInfo.ResourceInfo)adapter.getItem(position); + info.setEnabled(true); + } + + // pass to "account details" fragment + AccountDetailsFragment accountDetails = new AccountDetailsFragment(); + Bundle arguments = new Bundle(); + arguments.putSerializable(SelectCollectionsFragment.KEY_SERVER_INFO, serverInfo); + accountDetails.setArguments(arguments); + + getFragmentManager().beginTransaction() + .replace(R.id.fragment_container, accountDetails) + .addToBackStack(null) + .commitAllowingStateLoss(); + break; + default: + return false; + } + return true; + } + + + // input validation + + @Override + public void onPrepareOptionsMenu(Menu menu) { + boolean ok = false; + try { + ok = getListView().getCheckedItemCount() > 0; + } catch(IllegalStateException e) { + } + MenuItem item = menu.findItem(R.id.next); + item.setEnabled(ok); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java new file mode 100644 index 00000000..c1bf2fa2 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/SyncManager.java @@ -0,0 +1,214 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.syncadapter; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.HashSet; +import java.util.Set; + +import net.fortuna.ical4j.model.ValidationException; +import android.content.SyncResult; +import android.util.Log; +import at.bitfire.davdroid.ArrayUtils; +import at.bitfire.davdroid.resource.LocalCollection; +import at.bitfire.davdroid.resource.LocalStorageException; +import at.bitfire.davdroid.resource.RecordNotFoundException; +import at.bitfire.davdroid.resource.RemoteCollection; +import at.bitfire.davdroid.resource.Resource; +import at.bitfire.davdroid.webdav.DavException; +import at.bitfire.davdroid.webdav.HttpException; +import at.bitfire.davdroid.webdav.NotFoundException; +import at.bitfire.davdroid.webdav.PreconditionFailedException; + +public class SyncManager { + private static final String TAG = "davdroid.SyncManager"; + + private static final int MAX_MULTIGET_RESOURCES = 35; + + protected LocalCollection local; + protected RemoteCollection remote; + + + public SyncManager(LocalCollection local, RemoteCollection remote) { + this.local = local; + this.remote = remote; + } + + + public void synchronize(boolean manualSync, SyncResult syncResult) throws URISyntaxException, LocalStorageException, IOException, HttpException, DavException { + // PHASE 1: push local changes to server + int deletedRemotely = pushDeleted(), + addedRemotely = pushNew(), + updatedRemotely = pushDirty(); + + syncResult.stats.numEntries = deletedRemotely + addedRemotely + updatedRemotely; + + // PHASE 2A: check if there's a reason to do a sync with remote (= forced sync or remote CTag changed) + boolean fetchCollection = syncResult.stats.numEntries > 0; + if (manualSync) { + Log.i(TAG, "Synchronization forced"); + fetchCollection = true; + } + if (!fetchCollection) { + String currentCTag = remote.getCTag(), + lastCTag = local.getCTag(); + Log.d(TAG, "Last local CTag = " + lastCTag + "; current remote CTag = " + currentCTag); + if (currentCTag == null || !currentCTag.equals(lastCTag)) + fetchCollection = true; + } + + if (!fetchCollection) { + Log.i(TAG, "No local changes and CTags match, no need to sync"); + return; + } + + // PHASE 2B: detect details of remote changes + Log.i(TAG, "Fetching remote resource list"); + Set remotelyAdded = new HashSet(), + remotelyUpdated = new HashSet(); + + Resource[] remoteResources = remote.getMemberETags(); + for (Resource remoteResource : remoteResources) { + try { + Resource localResource = local.findByRemoteName(remoteResource.getName(), false); + if (localResource.getETag() == null || !localResource.getETag().equals(remoteResource.getETag())) + remotelyUpdated.add(remoteResource); + } catch(RecordNotFoundException e) { + remotelyAdded.add(remoteResource); + } + } + + // PHASE 3: pull remote changes from server + syncResult.stats.numInserts = pullNew(remotelyAdded.toArray(new Resource[0])); + syncResult.stats.numUpdates = pullChanged(remotelyUpdated.toArray(new Resource[0])); + syncResult.stats.numEntries += syncResult.stats.numInserts + syncResult.stats.numUpdates; + + Log.i(TAG, "Removing non-dirty resources that are not present remotely anymore"); + local.deleteAllExceptRemoteNames(remoteResources); + local.commit(); + + // update collection CTag + Log.i(TAG, "Sync complete, fetching new CTag"); + local.setCTag(remote.getCTag()); + } + + + private int pushDeleted() throws URISyntaxException, LocalStorageException, IOException, HttpException { + int count = 0; + long[] deletedIDs = local.findDeleted(); + + try { + Log.i(TAG, "Remotely removing " + deletedIDs.length + " deleted resource(s) (if not changed)"); + for (long id : deletedIDs) + try { + Resource res = local.findById(id, false); + if (res.getName() != null) // is this resource even present remotely? + try { + remote.delete(res); + } catch(NotFoundException e) { + Log.i(TAG, "Locally-deleted resource has already been removed from server"); + } catch(PreconditionFailedException e) { + Log.i(TAG, "Locally-deleted resource has been changed on the server in the meanwhile"); + } + + // always delete locally so that the record with the DELETED flag doesn't cause another deletion attempt + local.delete(res); + + count++; + } catch (RecordNotFoundException e) { + Log.wtf(TAG, "Couldn't read locally-deleted record", e); + } + } finally { + local.commit(); + } + return count; + } + + private int pushNew() throws URISyntaxException, LocalStorageException, IOException, HttpException { + int count = 0; + long[] newIDs = local.findNew(); + Log.i(TAG, "Uploading " + newIDs.length + " new resource(s) (if not existing)"); + try { + for (long id : newIDs) + try { + Resource res = local.findById(id, true); + String eTag = remote.add(res); + if (eTag != null) + local.updateETag(res, eTag); + local.clearDirty(res); + count++; + } catch(PreconditionFailedException e) { + Log.i(TAG, "Didn't overwrite existing resource with other content"); + } catch (ValidationException e) { + Log.e(TAG, "Couldn't create entity for adding: " + e.toString()); + } catch (RecordNotFoundException e) { + Log.wtf(TAG, "Couldn't read new record", e); + } + } finally { + local.commit(); + } + return count; + } + + private int pushDirty() throws URISyntaxException, LocalStorageException, IOException, HttpException { + int count = 0; + long[] dirtyIDs = local.findUpdated(); + Log.i(TAG, "Uploading " + dirtyIDs.length + " modified resource(s) (if not changed)"); + try { + for (long id : dirtyIDs) { + try { + Resource res = local.findById(id, true); + String eTag = remote.update(res); + if (eTag != null) + local.updateETag(res, eTag); + local.clearDirty(res); + count++; + } catch(PreconditionFailedException e) { + Log.i(TAG, "Locally changed resource has been changed on the server in the meanwhile"); + } catch (ValidationException e) { + Log.e(TAG, "Couldn't create entity for updating: " + e.toString()); + } catch (RecordNotFoundException e) { + Log.e(TAG, "Couldn't read dirty record", e); + } + } + } finally { + local.commit(); + } + return count; + } + + private int pullNew(Resource[] resourcesToAdd) throws URISyntaxException, LocalStorageException, IOException, HttpException, DavException { + int count = 0; + Log.i(TAG, "Fetching " + resourcesToAdd.length + " new remote resource(s)"); + + for (Resource[] resources : ArrayUtils.partition(resourcesToAdd, MAX_MULTIGET_RESOURCES)) + for (Resource res : remote.multiGet(resources)) { + Log.d(TAG, "Adding " + res.getName()); + local.add(res); + local.commit(); + count++; + } + return count; + } + + private int pullChanged(Resource[] resourcesToUpdate) throws URISyntaxException, LocalStorageException, IOException, HttpException, DavException { + int count = 0; + Log.i(TAG, "Fetching " + resourcesToUpdate.length + " updated remote resource(s)"); + + for (Resource[] resources : ArrayUtils.partition(resourcesToUpdate, MAX_MULTIGET_RESOURCES)) + for (Resource res : remote.multiGet(resources)) { + Log.i(TAG, "Updating " + res.getName()); + local.updateByRemoteName(res); + local.commit(); + count++; + } + return count; + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/syncadapter/WebDavResourceAdapter.java b/app/src/main/java/at/bitfire/davdroid/syncadapter/WebDavResourceAdapter.java new file mode 100644 index 00000000..9263874d --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/syncadapter/WebDavResourceAdapter.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.syncadapter; + +import java.util.List; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; +import at.bitfire.davdroid.webdav.WebDavResource; + +public class WebDavResourceAdapter extends BaseAdapter { + protected int viewId; + protected LayoutInflater inflater; + WebDavResource[] items; + + public WebDavResourceAdapter(Context context, int textViewResourceId, List objects) { + viewId = textViewResourceId; + inflater = LayoutInflater.from(context); + items = objects.toArray(new WebDavResource[0]); + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + WebDavResource item = items[position]; + View itemView = (View)inflater.inflate(viewId, null); + + TextView textName = (TextView) itemView.findViewById(android.R.id.text1); + textName.setText(item.getDisplayName()); + + TextView textDescription = (TextView) itemView.findViewById(android.R.id.text2); + String description = item.getDescription(); + if (description == null) + description = item.getLocation().getPath(); + textDescription.setText(description); + + return itemView; + } + + @Override + public int getCount() { + return items.length; + } + + @Override + public Object getItem(int position) { + return items[position]; + } + + @Override + public long getItemId(int position) { + return position; + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavAddressbookMultiget.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavAddressbookMultiget.java new file mode 100644 index 00000000..9b32960f --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavAddressbookMultiget.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import org.simpleframework.xml.Namespace; +import org.simpleframework.xml.NamespaceList; +import org.simpleframework.xml.Root; + +@Root(name="addressbook-multiget") +@NamespaceList({ + @Namespace(reference="DAV:"), + @Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav") +}) +@Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav") +public class DavAddressbookMultiget extends DavMultiget { +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavCalendarMultiget.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavCalendarMultiget.java new file mode 100644 index 00000000..75a7ce71 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavCalendarMultiget.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import org.simpleframework.xml.Namespace; +import org.simpleframework.xml.NamespaceList; +import org.simpleframework.xml.Root; + +@Root(name="calendar-multiget") +@NamespaceList({ + @Namespace(reference="DAV:"), + @Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav") +}) +@Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav") +public class DavCalendarMultiget extends DavMultiget { +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavException.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavException.java new file mode 100644 index 00000000..506af6da --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavException.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +public class DavException extends Exception { + private static final long serialVersionUID = -2118919144443165706L; + + final private static String prefix = "Invalid DAV response: "; + + /* used to indiciate DAV protocol errors */ + + + public DavException(String message) { + super(prefix + message); + } + + public DavException(String message, Throwable ex) { + super(prefix + message, ex); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavHref.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavHref.java new file mode 100644 index 00000000..438c4e49 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavHref.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import org.simpleframework.xml.Namespace; +import org.simpleframework.xml.Root; +import org.simpleframework.xml.Text; + +@Root(name="href") +@Namespace(prefix="D",reference="DAV:") +public class DavHref { + @Text + String href; + + DavHref() { + } + + public DavHref(String href) { + this.href = href; + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpClient.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpClient.java new file mode 100644 index 00000000..9a20e6f6 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpClient.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import android.util.Log; +import at.bitfire.davdroid.Constants; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.ManagedHttpClientConnectionFactory; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; + +import java.lang.reflect.Method; + + +public class DavHttpClient { + private final static String TAG = "davdroid.DavHttpClient"; + + private final static RequestConfig defaultRqConfig; + private final static Registry socketFactoryRegistry; + + static { + socketFactoryRegistry = RegistryBuilder. create() + .register("http", PlainConnectionSocketFactory.getSocketFactory()) + .register("https", TlsSniSocketFactory.INSTANCE) + .build(); + + // use request defaults from AndroidHttpClient + defaultRqConfig = RequestConfig.copy(RequestConfig.DEFAULT) + .setConnectTimeout(20*1000) + .setSocketTimeout(45*1000) + .setStaleConnectionCheckEnabled(false) + .build(); + } + + + public static CloseableHttpClient create(boolean disableCompression, boolean logTraffic) { + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); + // limits per DavHttpClient (= per DavSyncAdapter extends AbstractThreadedSyncAdapter) + connectionManager.setMaxTotal(3); // max. 3 connections in total + connectionManager.setDefaultMaxPerRoute(2); // max. 2 connections per host + + HttpClientBuilder builder = HttpClients.custom() + .useSystemProperties() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(defaultRqConfig) + .setRetryHandler(DavHttpRequestRetryHandler.INSTANCE) + .setRedirectStrategy(DavRedirectStrategy.INSTANCE) + .setUserAgent("DAVdroid/" + Constants.APP_VERSION) + .disableCookieManagement(); + + if (Log.isLoggable("Wire", Log.DEBUG)) { + Log.i(TAG, "Wire logging active, disabling HTTP compression"); + builder = builder.disableContentCompression(); + } + + return builder.build(); + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpRequestRetryHandler.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpRequestRetryHandler.java new file mode 100644 index 00000000..fdbcc05c --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavHttpRequestRetryHandler.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import java.util.Locale; + +import org.apache.commons.lang.ArrayUtils; + +import org.apache.http.HttpRequest; +import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; + +public class DavHttpRequestRetryHandler extends DefaultHttpRequestRetryHandler { + final static DavHttpRequestRetryHandler INSTANCE = new DavHttpRequestRetryHandler(); + + // see http://www.iana.org/assignments/http-methods/http-methods.xhtml + private final static String idempotentMethods[] = { + "DELETE", "GET", "HEAD", "MKCALENDAR", "MKCOL", "OPTIONS", "PROPFIND", "PROPPATCH", + "PUT", "REPORT", "SEARCH", "TRACE" + }; + + public DavHttpRequestRetryHandler() { + super(/* retry count */ 3, /* retry already sent requests? */ false); + } + + //@Override + protected boolean handleAsIdempotent(final HttpRequest request) { + final String method = request.getRequestLine().getMethod().toUpperCase(Locale.ROOT); + return ArrayUtils.contains(idempotentMethods, method); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavIncapableException.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavIncapableException.java new file mode 100644 index 00000000..794865ec --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavIncapableException.java @@ -0,0 +1,18 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +public class DavIncapableException extends DavException { + private static final long serialVersionUID = -7199786680939975667L; + + /* used to indicate that the server doesn't support DAV */ + + public DavIncapableException(String msg) { + super(msg); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavMultiget.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavMultiget.java new file mode 100644 index 00000000..64162b19 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavMultiget.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import java.util.ArrayList; +import java.util.List; + +import org.simpleframework.xml.Element; +import org.simpleframework.xml.ElementList; +import org.simpleframework.xml.Order; + +@Order(elements={"prop","href"}) +public class DavMultiget { + public enum Type { + ADDRESS_BOOK, + CALENDAR + } + + @Element + DavProp prop; + + @ElementList(inline=true) + List hrefs; + + + public static DavMultiget newRequest(Type type, String names[]) { + DavMultiget multiget = (type == Type.ADDRESS_BOOK) ? new DavAddressbookMultiget() : new DavCalendarMultiget(); + + multiget.prop = new DavProp(); + multiget.prop.getetag = new DavProp.GetETag(); + + if (type == Type.ADDRESS_BOOK) + multiget.prop.addressData = new DavProp.AddressData(); + else if (type == Type.CALENDAR) + multiget.prop.calendarData = new DavProp.CalendarData(); + + multiget.hrefs = new ArrayList(names.length); + for (String name : names) + multiget.hrefs.add(new DavHref(name)); + + return multiget; + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavMultistatus.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavMultistatus.java new file mode 100644 index 00000000..dce27c2b --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavMultistatus.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import java.util.List; + +import org.simpleframework.xml.ElementList; +import org.simpleframework.xml.Namespace; +import org.simpleframework.xml.Root; + +@Namespace(reference="DAV:") +@Root(strict=false) +public class DavMultistatus { + @ElementList(inline=true,entry="response",required=false) + List response; +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavNoContentException.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavNoContentException.java new file mode 100644 index 00000000..6e3464b9 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavNoContentException.java @@ -0,0 +1,18 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +public class DavNoContentException extends DavException { + private static final long serialVersionUID = 6256645020350945477L; + + private final static String message = "HTTP response entity (content) expected but not received"; + + public DavNoContentException() { + super(message); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavNoMultiStatusException.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavNoMultiStatusException.java new file mode 100644 index 00000000..07eb6e18 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavNoMultiStatusException.java @@ -0,0 +1,18 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +public class DavNoMultiStatusException extends DavException { + private static final long serialVersionUID = -3600405724694229828L; + + private final static String message = "207 Multi-Status expected but not received"; + + public DavNoMultiStatusException() { + super(message); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavProp.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavProp.java new file mode 100644 index 00000000..93f25645 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavProp.java @@ -0,0 +1,211 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import java.util.List; + +import lombok.Getter; + +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.Element; +import org.simpleframework.xml.ElementList; +import org.simpleframework.xml.Namespace; +import org.simpleframework.xml.Root; +import org.simpleframework.xml.Text; + +@Namespace(prefix="D",reference="DAV:") +@Root(strict=false) +public class DavProp { + + /* RFC 4918 WebDAV */ + + @Element(required=false) + ResourceType resourcetype; + + @Element(required=false) + DisplayName displayname; + + @Element(required=false) + GetCTag getctag; + + @Element(required=false) + GetETag getetag; + + @Root(strict=false) + public static class ResourceType { + @Element(required=false) + @Getter private Collection collection; + public static class Collection { } + + @Element(required=false) + @Getter private Addressbook addressbook; + @Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav") + public static class Addressbook { } + + @Element(required=false) + @Getter private Calendar calendar; + @Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav") + public static class Calendar { } + } + + public static class DisplayName { + @Text(required=false) + @Getter private String displayName; + } + + @Namespace(prefix="CS",reference="http://calendarserver.org/ns/") + public static class GetCTag { + @Text(required=false) + @Getter private String CTag; + } + + public static class GetETag { + @Text(required=false) + @Getter private String ETag; + } + + + /* RFC 5397 WebDAV Current Principal Extension */ + + @Element(required=false,name="current-user-principal") + CurrentUserPrincipal currentUserPrincipal; + + public static class CurrentUserPrincipal { + @Element(required=false) + @Getter private DavHref href; + } + + + /* RFC 3744 WebDAV Access Control Protocol */ + + @ElementList(required=false,name="current-user-privilege-set",entry="privilege") + List currentUserPrivilegeSet; + + public static class Privilege { + @Element(required=false) + @Getter private PrivAll all; + + @Element(required=false) + @Getter private PrivBind bind; + + @Element(required=false) + @Getter private PrivUnbind unbind; + + @Element(required=false) + @Getter private PrivWrite write; + + @Element(required=false,name="write-content") + @Getter private PrivWriteContent writeContent; + + public static class PrivAll { } + public static class PrivBind { } + public static class PrivUnbind { } + public static class PrivWrite { } + public static class PrivWriteContent { } + } + + + + /* RFC 4791 CalDAV, RFC 6352 CardDAV */ + + @Element(required=false,name="addressbook-home-set") + AddressbookHomeSet addressbookHomeSet; + + @Element(required=false,name="calendar-home-set") + CalendarHomeSet calendarHomeSet; + + @Element(required=false,name="addressbook-description") + AddressbookDescription addressbookDescription; + + @Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav") + @ElementList(required=false,name="supported-address-data",entry="address-data-type") + List supportedAddressData; + + @Element(required=false,name="calendar-description") + CalendarDescription calendarDescription; + + @Element(required=false,name="calendar-color") + CalendarColor calendarColor; + + @Element(required=false,name="calendar-timezone") + CalendarTimezone calendarTimezone; + + @Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav") + @ElementList(required=false,name="supported-calendar-component-set",entry="comp") + List supportedCalendarComponentSet; + + @Element(name="address-data",required=false) + AddressData addressData; + + @Element(name="calendar-data",required=false) + CalendarData calendarData; + + + @Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav") + public static class AddressbookHomeSet { + @Element(required=false) + @Getter private DavHref href; + } + + @Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav") + public static class CalendarHomeSet { + @Element(required=false) + @Getter private DavHref href; + } + + @Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav") + public static class AddressbookDescription { + @Text(required=false) + @Getter private String description; + } + + @Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav") + public static class AddressDataType { + @Attribute(name="content-type") + @Getter private String contentType; + + @Attribute + @Getter private String version; + } + + @Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav") + public static class CalendarDescription { + @Text(required=false) + @Getter private String description; + } + + @Namespace(prefix="A",reference="http://apple.com/ns/ical/") + public static class CalendarColor { + @Text(required=false) + @Getter private String color; + } + + @Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav") + public static class CalendarTimezone { + @Text(required=false) + @Getter private String timezone; + } + + @Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav") + public static class Comp { + @Attribute + @Getter String name; + } + + @Namespace(prefix="CD",reference="urn:ietf:params:xml:ns:carddav") + public static class AddressData { + @Text(required=false) + @Getter String vcard; + } + + @Namespace(prefix="C",reference="urn:ietf:params:xml:ns:caldav") + public static class CalendarData { + @Text(required=false) + @Getter String ical; + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavPropfind.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavPropfind.java new file mode 100644 index 00000000..abf4104e --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavPropfind.java @@ -0,0 +1,19 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import org.simpleframework.xml.Element; +import org.simpleframework.xml.Namespace; +import org.simpleframework.xml.Root; + +@Namespace(reference="DAV:") +@Root(name="propfind") +public class DavPropfind { + @Element(required=false) + protected DavProp prop; +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavPropstat.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavPropstat.java new file mode 100644 index 00000000..74ee66d6 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavPropstat.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import org.simpleframework.xml.Element; +import org.simpleframework.xml.Root; + +@Root(strict=false,name="propstat") +public class DavPropstat { + @Element + DavProp prop; + + @Element + String status; +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavRedirectStrategy.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavRedirectStrategy.java new file mode 100644 index 00000000..6e4e7d47 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavRedirectStrategy.java @@ -0,0 +1,105 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import java.net.URI; +import java.net.URISyntaxException; + +import android.util.Log; +import at.bitfire.davdroid.URIUtils; +import org.apache.http.Header; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.ProtocolException; +import org.apache.http.RequestLine; +import org.apache.http.client.RedirectStrategy; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.protocol.HttpContext; + +/** + * Custom Redirect Strategy that handles 30x for CalDAV/CardDAV-specific requests correctly + */ +public class DavRedirectStrategy implements RedirectStrategy { + private final static String TAG = "davdroid.DavRedirectStrategy"; + final static DavRedirectStrategy INSTANCE = new DavRedirectStrategy(); + + protected final static String REDIRECTABLE_METHODS[] = { + "OPTIONS", "GET", "PUT", "DELETE" + }; + + + @Override + public HttpUriRequest getRedirect(HttpRequest request, HttpResponse response, HttpContext context) throws ProtocolException { + RequestLine line = request.getRequestLine(); + + String location = getLocation(request, response, context).toString(); + Log.i(TAG, "Following redirection: " + line.getMethod() + " " + line.getUri() + " -> " + location); + + return RequestBuilder.copy(request) + .setUri(location) + .removeHeaders("Content-Length") // Content-Length will be set again automatically, if required; + // remove it now to avoid duplicate header + .build(); + } + + /** + * Determines whether a response indicates a redirection and if it does, whether to follow this redirection. + * PROPFIND and REPORT must handle redirections explicitely because multi-status processing requires knowledge of the content location. + * @return true for 3xx responses on OPTIONS, GET, PUT, DELETE requests that have a valid Location header; false otherwise + */ + @Override + public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context) throws ProtocolException { + if (response.getStatusLine().getStatusCode()/100 == 3) { + boolean redirectable = false; + for (String method : REDIRECTABLE_METHODS) + if (method.equalsIgnoreCase(request.getRequestLine().getMethod())) { + redirectable = true; + break; + } + return redirectable && getLocation(request, response, context) != null; + } + return false; + } + + /** + * Gets the destination of a redirection + * @return absolute URL of new location; null if not available + */ + static URI getLocation(HttpRequest request, HttpResponse response, HttpContext context) { + Header locationHdr = response.getFirstHeader("Location"); + if (locationHdr == null) { + Log.e(TAG, "Received redirection without Location header, ignoring"); + return null; + } + try { + URI location = URIUtils.parseURI(locationHdr.getValue()); + + // some servers don't return absolute URLs as required by RFC 2616 + if (!location.isAbsolute()) { + Log.w(TAG, "Received invalid redirection to relative URL, repairing"); + URI originalURI = URIUtils.parseURI(request.getRequestLine().getUri()); + if (!originalURI.isAbsolute()) { + final HttpHost target = HttpClientContext.adapt(context).getTargetHost(); + if (target != null) + originalURI = org.apache.http.client.utils.URIUtils.rewriteURI(originalURI, target); + else + return null; + } + return originalURI.resolve(location); + } + return location; + } catch (URISyntaxException e) { + Log.e(TAG, "Received redirection from/to invalid URI, ignoring", e); + } + return null; + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/DavResponse.java b/app/src/main/java/at/bitfire/davdroid/webdav/DavResponse.java new file mode 100644 index 00000000..b1ba1fd9 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/DavResponse.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import java.util.List; + +import lombok.Getter; + +import org.simpleframework.xml.Element; +import org.simpleframework.xml.ElementList; +import org.simpleframework.xml.Root; + +@Root(strict=false) +public class DavResponse { + @Element + @Getter DavHref href; + + @ElementList(inline=true) + @Getter List propstat; +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/HttpException.java b/app/src/main/java/at/bitfire/davdroid/webdav/HttpException.java new file mode 100644 index 00000000..79b48164 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/HttpException.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import lombok.Getter; + +public class HttpException extends org.apache.http.HttpException { + private static final long serialVersionUID = -4805778240079377401L; + + @Getter private int code; + + HttpException(int code, String message) { + super(message); + this.code = code; + } + + public boolean isClientError() { + return code/100 == 4; + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/HttpPropfind.java b/app/src/main/java/at/bitfire/davdroid/webdav/HttpPropfind.java new file mode 100644 index 00000000..677905ed --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/HttpPropfind.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import java.io.StringWriter; +import java.net.URI; +import java.util.LinkedList; + +import org.simpleframework.xml.Serializer; +import org.simpleframework.xml.core.Persister; + +import android.util.Log; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.entity.StringEntity; + +public class HttpPropfind extends HttpEntityEnclosingRequestBase { + private static final String TAG = "davdroid.HttpPropfind"; + + public final static String METHOD_NAME = "PROPFIND"; + + public enum Mode { + CURRENT_USER_PRINCIPAL, + HOME_SETS, + CARDDAV_COLLECTIONS, + CALDAV_COLLECTIONS, + COLLECTION_CTAG, + MEMBERS_ETAG + } + + + HttpPropfind(URI uri) { + setURI(uri); + } + + HttpPropfind(URI uri, Mode mode) { + this(uri); + + DavPropfind propfind = new DavPropfind(); + propfind.prop = new DavProp(); + + int depth = 0; + switch (mode) { + case CURRENT_USER_PRINCIPAL: + propfind.prop.currentUserPrincipal = new DavProp.CurrentUserPrincipal(); + break; + case HOME_SETS: + propfind.prop.addressbookHomeSet = new DavProp.AddressbookHomeSet(); + propfind.prop.calendarHomeSet = new DavProp.CalendarHomeSet(); + break; + case CARDDAV_COLLECTIONS: + depth = 1; + propfind.prop.displayname = new DavProp.DisplayName(); + propfind.prop.resourcetype = new DavProp.ResourceType(); + propfind.prop.currentUserPrivilegeSet = new LinkedList(); + propfind.prop.addressbookDescription = new DavProp.AddressbookDescription(); + propfind.prop.supportedAddressData = new LinkedList(); + break; + case CALDAV_COLLECTIONS: + depth = 1; + propfind.prop.displayname = new DavProp.DisplayName(); + propfind.prop.resourcetype = new DavProp.ResourceType(); + propfind.prop.currentUserPrivilegeSet = new LinkedList(); + propfind.prop.calendarDescription = new DavProp.CalendarDescription(); + propfind.prop.calendarColor = new DavProp.CalendarColor(); + propfind.prop.calendarTimezone = new DavProp.CalendarTimezone(); + propfind.prop.supportedCalendarComponentSet = new LinkedList(); + break; + case COLLECTION_CTAG: + propfind.prop.getctag = new DavProp.GetCTag(); + break; + case MEMBERS_ETAG: + depth = 1; + propfind.prop.getctag = new DavProp.GetCTag(); + propfind.prop.getetag = new DavProp.GetETag(); + break; + } + + try { + Serializer serializer = new Persister(); + StringWriter writer = new StringWriter(); + serializer.write(propfind, writer); + + setHeader("Content-Type", "text/xml; charset=UTF-8"); + setHeader("Accept", "text/xml"); + setHeader("Depth", String.valueOf(depth)); + setEntity(new StringEntity(writer.toString(), "UTF-8")); + } catch(Exception ex) { + Log.e(TAG, "Couldn't prepare PROPFIND request for " + uri, ex); + abort(); + } + } + + @Override + public String getMethod() { + return METHOD_NAME; + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/HttpReport.java b/app/src/main/java/at/bitfire/davdroid/webdav/HttpReport.java new file mode 100644 index 00000000..d7323818 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/HttpReport.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import android.util.Log; + +import java.io.UnsupportedEncodingException; +import java.net.URI; + +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.entity.StringEntity; + +public class HttpReport extends HttpEntityEnclosingRequestBase { + private static final String TAG = "davdroid.HttpEntityEncloseRequestBase"; + + public final static String METHOD_NAME = "REPORT"; + + + HttpReport(URI uri) { + setURI(uri); + } + + HttpReport(URI uri, String entity) { + this(uri); + + setHeader("Content-Type", "text/xml; charset=UTF-8"); + setHeader("Accept", "text/xml"); + setHeader("Depth", "0"); + + try { + setEntity(new StringEntity(entity, "UTF-8")); + } catch (UnsupportedEncodingException e) { + Log.wtf(TAG, "String entity doesn't support UTF-8"); + } + } + + @Override + public String getMethod() { + return METHOD_NAME; + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/NotAuthorizedException.java b/app/src/main/java/at/bitfire/davdroid/webdav/NotAuthorizedException.java new file mode 100644 index 00000000..4eab6adb --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/NotAuthorizedException.java @@ -0,0 +1,19 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import org.apache.http.HttpStatus; + +public class NotAuthorizedException extends HttpException { + private static final long serialVersionUID = 2490525047224413586L; + + public NotAuthorizedException(String reason) { + super(HttpStatus.SC_UNAUTHORIZED, reason); + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/NotFoundException.java b/app/src/main/java/at/bitfire/davdroid/webdav/NotFoundException.java new file mode 100644 index 00000000..f612d733 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/NotFoundException.java @@ -0,0 +1,18 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import org.apache.http.HttpStatus; + +public class NotFoundException extends HttpException { + private static final long serialVersionUID = 1565961502781880483L; + + public NotFoundException(String reason) { + super(HttpStatus.SC_NOT_FOUND, reason); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/PreconditionFailedException.java b/app/src/main/java/at/bitfire/davdroid/webdav/PreconditionFailedException.java new file mode 100644 index 00000000..064ff38a --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/PreconditionFailedException.java @@ -0,0 +1,18 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import org.apache.http.HttpStatus; + +public class PreconditionFailedException extends HttpException { + private static final long serialVersionUID = 102282229174086113L; + + public PreconditionFailedException(String reason) { + super(HttpStatus.SC_PRECONDITION_FAILED, reason); + } +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/TlsSniSocketFactory.java b/app/src/main/java/at/bitfire/davdroid/webdav/TlsSniSocketFactory.java new file mode 100644 index 00000000..3cd46f25 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/TlsSniSocketFactory.java @@ -0,0 +1,186 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import org.apache.commons.lang.StringUtils; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.net.SSLCertificateSocketFactory; +import android.os.Build; +import android.util.Log; +import org.apache.http.HttpHost; +import org.apache.http.conn.socket.LayeredConnectionSocketFactory; +import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier; +import org.apache.http.protocol.HttpContext; + +public class TlsSniSocketFactory implements LayeredConnectionSocketFactory { + private static final String TAG = "davdroid.SNISocketFactory"; + + final static TlsSniSocketFactory INSTANCE = new TlsSniSocketFactory(); + + private final static SSLCertificateSocketFactory sslSocketFactory = + (SSLCertificateSocketFactory)SSLCertificateSocketFactory.getDefault(0); + private final static HostnameVerifier hostnameVerifier = new BrowserCompatHostnameVerifier(); + + + /* + For SSL connections without HTTP(S) proxy: + 1) createSocket() is called + 2) connectSocket() is called which creates a new SSL connection + 2a) SNI is set up, and then + 2b) the connection is established, hands are shaken and certificate/host name are verified + + Layered sockets are used with HTTP(S) proxies: + 1) a new plain socket is created by the HTTP library + 2) the plain socket is connected to http://proxy:8080 + 3) a CONNECT request is sent to the proxy and the response is parsed + 4) now, createLayeredSocket() is called which wraps an SSL socket around the proxy connection, + doing all the set-up and verfication + 4a) Because SSLSocket.createSocket(socket, ...) always does a handshake without allowing + to set up SNI before, *** SNI is not available for layered connections *** (unless + active by Android's defaults, which it isn't at the moment). + */ + + + @Override + public Socket createSocket(HttpContext context) throws IOException { + SSLSocket ssl = (SSLSocket)sslSocketFactory.createSocket(); + setReasonableEncryption(ssl); + return ssl; + } + + @Override + public Socket connectSocket(int timeout, Socket plain, HttpHost host, InetSocketAddress remoteAddr, InetSocketAddress localAddr, HttpContext context) throws IOException { + Log.d(TAG, "Preparing direct SSL connection (without proxy) to " + host); + + // we'll rather use an SSLSocket directly + plain.close(); + + // create a plain SSL socket, but don't do hostname/certificate verification yet + SSLSocket ssl = (SSLSocket)sslSocketFactory.createSocket(remoteAddr.getAddress(), host.getPort()); + setReasonableEncryption(ssl); + + // connect, set SNI, shake hands, verify, print connection info + connectWithSNI(ssl, host.getHostName()); + + return ssl; + } + + @Override + public Socket createLayeredSocket(Socket plain, String host, int port, HttpContext context) throws IOException, UnknownHostException { + Log.d(TAG, "Preparing layered SSL connection (over proxy) to " + host); + + // create a layered SSL socket, but don't do hostname/certificate verification yet + SSLSocket ssl = (SSLSocket)sslSocketFactory.createSocket(plain, host, port, true); + setReasonableEncryption(ssl); + + // already connected, but verify host name again and print some connection info + Log.w(TAG, "Setting SNI/TLSv1.2 will silently fail because the handshake is already done"); + connectWithSNI(ssl, host); + + return ssl; + } + + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private void connectWithSNI(SSLSocket ssl, String host) throws SSLPeerUnverifiedException { + // - set SNI host name + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + Log.d(TAG, "Using documented SNI with host name " + host); + sslSocketFactory.setHostname(ssl, host); + } else { + Log.d(TAG, "No documented SNI support on Android <4.2, trying with reflection"); + try { + java.lang.reflect.Method setHostnameMethod = ssl.getClass().getMethod("setHostname", String.class); + setHostnameMethod.invoke(ssl, host); + } catch (Exception e) { + Log.w(TAG, "SNI not useable", e); + } + } + + // verify hostname and certificate + SSLSession session = ssl.getSession(); + if (!hostnameVerifier.verify(host, session)) + throw new SSLPeerUnverifiedException("Cannot verify hostname: " + host); + + Log.d(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() + + " using " + session.getCipherSuite()); + } + + + @SuppressLint("DefaultLocale") + private void setReasonableEncryption(SSLSocket ssl) { + // set reasonable SSL/TLS settings before the handshake + + // Android 5.0+ (API level21) provides reasonable default settings + // but it still allows SSLv3 + // https://developer.android.com/about/versions/android-5.0-changes.html#ssl + + // - enable all supported protocols (enables TLSv1.1 and TLSv1.2 on Android <5.0, if available) + // - remove all SSL versions (especially SSLv3) because they're insecure now + List protocols = new LinkedList(); + for (String protocol : ssl.getSupportedProtocols()) + if (!protocol.toUpperCase().contains("SSL")) + protocols.add(protocol); + Log.v(TAG, "Setting allowed TLS protocols: " + StringUtils.join(protocols, ", ")); + ssl.setEnabledProtocols(protocols.toArray(new String[0])); + + if (android.os.Build.VERSION.SDK_INT < 21) { + // choose secure cipher suites + List allowedCiphers = Arrays.asList(new String[] { + // allowed secure ciphers according to NIST.SP.800-52r1.pdf Section 3.3.1 (see docs directory) + // TLS 1.2 + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECHDE_RSA_WITH_AES_128_GCM_SHA256", + // maximum interoperability + "TLS_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_RSA_WITH_AES_128_CBC_SHA", + // additionally + "TLS_RSA_WITH_AES_256_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + }); + + List availableCiphers = Arrays.asList(ssl.getSupportedCipherSuites()); + + // preferred ciphers = allowed Ciphers \ availableCiphers + HashSet preferredCiphers = new HashSet(allowedCiphers); + preferredCiphers.retainAll(availableCiphers); + + // add preferred ciphers to enabled ciphers + // for maximum security, preferred ciphers should *replace* enabled ciphers, + // but I guess for the security level of DAVdroid, disabling of insecure + // ciphers should be a server-side task + HashSet enabledCiphers = preferredCiphers; + enabledCiphers.addAll(new HashSet(Arrays.asList(ssl.getEnabledCipherSuites()))); + + Log.v(TAG, "Setting allowed TLS ciphers: " + StringUtils.join(enabledCiphers, ", ")); + ssl.setEnabledCipherSuites(enabledCiphers.toArray(new String[0])); + } + } + +} diff --git a/app/src/main/java/at/bitfire/davdroid/webdav/WebDavResource.java b/app/src/main/java/at/bitfire/davdroid/webdav/WebDavResource.java new file mode 100644 index 00000000..5643f8e4 --- /dev/null +++ b/app/src/main/java/at/bitfire/davdroid/webdav/WebDavResource.java @@ -0,0 +1,581 @@ +/******************************************************************************* + * Copyright (c) 2014 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 + ******************************************************************************/ +package at.bitfire.davdroid.webdav; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import lombok.Cleanup; +import lombok.Getter; +import lombok.ToString; + +import org.apache.commons.lang.StringUtils; +import org.simpleframework.xml.Serializer; +import org.simpleframework.xml.core.Persister; + +import android.util.Log; +import at.bitfire.davdroid.URIUtils; +import at.bitfire.davdroid.resource.Event; +import at.bitfire.davdroid.webdav.DavProp.Comp; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.StatusLine; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.AuthCache; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpOptions; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.impl.client.BasicAuthCache; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicLineParser; +import org.apache.http.util.EntityUtils; +import ezvcard.VCardVersion; + + +/** + * Represents a WebDAV resource (file or collection). + * This class is used for all CalDAV/CardDAV communcation. + */ +@ToString +public class WebDavResource { + private static final String TAG = "davdroid.WebDavResource"; + + public enum Property { + CURRENT_USER_PRINCIPAL, // resource detection + ADDRESSBOOK_HOMESET, CALENDAR_HOMESET, + CONTENT_TYPE, READ_ONLY, // WebDAV (common) + DISPLAY_NAME, DESCRIPTION, ETAG, + IS_COLLECTION, CTAG, // collections + IS_CALENDAR, COLOR, TIMEZONE, // CalDAV + IS_ADDRESSBOOK, VCARD_VERSION // CardDAV + } + public enum PutMode { + ADD_DONT_OVERWRITE, + UPDATE_DONT_OVERWRITE + } + + // location of this resource + @Getter protected URI location; + + // DAV capabilities (DAV: header) and allowed DAV methods (set for OPTIONS request) + protected Set capabilities = new HashSet(), + methods = new HashSet(); + + // DAV properties + protected HashMap properties = new HashMap(); + @Getter protected List supportedComponents; + + // list of members (only for collections) + @Getter protected List members; + + // content (available after GET) + @Getter protected byte[] content; + + protected CloseableHttpClient httpClient; + protected HttpClientContext context; + + + public WebDavResource(CloseableHttpClient httpClient, URI baseURI) { + this.httpClient = httpClient; + location = baseURI; + + context = HttpClientContext.create(); + context.setCredentialsProvider(new BasicCredentialsProvider()); + } + + public WebDavResource(CloseableHttpClient httpClient, URI baseURI, String username, String password, boolean preemptive) { + this(httpClient, baseURI); + + HttpHost host = new HttpHost(baseURI.getHost(), baseURI.getPort(), baseURI.getScheme()); + context.getCredentialsProvider().setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password)); + + if (preemptive) { + Log.d(TAG, "Using preemptive authentication (not compatible with Digest auth)"); + AuthCache authCache = context.getAuthCache(); + if (authCache == null) + authCache = new BasicAuthCache(); + authCache.put(host, new BasicScheme()); + context.setAuthCache(authCache); + } + } + + WebDavResource(WebDavResource parent) { // copy constructor: based on existing WebDavResource, reuse settings + httpClient = parent.httpClient; + context = parent.context; + location = parent.location; + } + + protected WebDavResource(WebDavResource parent, URI url) { + this(parent); + location = parent.location.resolve(url); + } + + public WebDavResource(WebDavResource parent, String member) throws URISyntaxException { + this(parent); + location = parent.location.resolve(URIUtils.parseURI(member)); + } + + public WebDavResource(WebDavResource parent, String member, String ETag) throws URISyntaxException { + this(parent, member); + properties.put(Property.ETAG, ETag); + } + + + + /* feature detection */ + + public void options() throws URISyntaxException, IOException, HttpException { + HttpOptions options = new HttpOptions(location); + CloseableHttpResponse response = httpClient.execute(options, context); + try { + checkResponse(response); + + Header[] allowHeaders = response.getHeaders("Allow"); + for (Header allowHeader : allowHeaders) + methods.addAll(Arrays.asList(allowHeader.getValue().split(", ?"))); + + Header[] capHeaders = response.getHeaders("DAV"); + for (Header capHeader : capHeaders) + capabilities.addAll(Arrays.asList(capHeader.getValue().split(", ?"))); + } finally { + response.close(); + } + } + + public boolean supportsDAV(String capability) { + return capabilities.contains(capability); + } + + public boolean supportsMethod(String method) { + return methods.contains(method); + } + + + /* file hierarchy methods */ + + public String getName() { + String[] names = StringUtils.split(location.getPath(), "/"); + return names[names.length - 1]; + } + + + /* property methods */ + + public String getCurrentUserPrincipal() { + return properties.get(Property.CURRENT_USER_PRINCIPAL); + } + + public String getAddressbookHomeSet() { + return properties.get(Property.ADDRESSBOOK_HOMESET); + } + + public String getCalendarHomeSet() { + return properties.get(Property.CALENDAR_HOMESET); + } + + public String getContentType() { + return properties.get(Property.CONTENT_TYPE); + } + + public void setContentType(String mimeType) { + properties.put(Property.CONTENT_TYPE, mimeType); + } + + public boolean isReadOnly() { + return properties.containsKey(Property.READ_ONLY); + } + + public String getDisplayName() { + return properties.get(Property.DISPLAY_NAME); + } + + public String getDescription() { + return properties.get(Property.DESCRIPTION); + } + + public String getCTag() { + return properties.get(Property.CTAG); + } + public void invalidateCTag() { + properties.remove(Property.CTAG); + } + + public String getETag() { + return properties.get(Property.ETAG); + } + + public boolean isCalendar() { + return properties.containsKey(Property.IS_CALENDAR); + } + + public String getColor() { + return properties.get(Property.COLOR); + } + + public String getTimezone() { + return properties.get(Property.TIMEZONE); + } + + public boolean isAddressBook() { + return properties.containsKey(Property.IS_ADDRESSBOOK); + } + + public VCardVersion getVCardVersion() { + String versionStr = properties.get(Property.VCARD_VERSION); + return (versionStr != null) ? VCardVersion.valueOfByStr(versionStr) : null; + } + + + /* collection operations */ + + public void propfind(HttpPropfind.Mode mode) throws URISyntaxException, IOException, DavException, HttpException { + CloseableHttpResponse response = null; + + // processMultiStatus() requires knowledge of the actual content location, + // so we have to handle redirections manually and create a new request for the new location + for (int i = context.getRequestConfig().getMaxRedirects(); i > 0; i--) { + HttpPropfind propfind = new HttpPropfind(location, mode); + response = httpClient.execute(propfind, context); + + if (response.getStatusLine().getStatusCode()/100 == 3) { + location = DavRedirectStrategy.getLocation(propfind, response, context); + Log.i(TAG, "Redirection on PROPFIND; trying again at new content URL: " + location); + // don't forget to throw away the unneeded response content + HttpEntity entity = response.getEntity(); + if (entity != null) { @Cleanup InputStream content = entity.getContent(); } + } else + break; // answer was NOT a redirection, continue + } + if (response == null) + throw new DavNoContentException(); + + try { + checkResponse(response); // will also handle Content-Location + processMultiStatus(response); + } finally { + response.close(); + } + } + + public void multiGet(DavMultiget.Type type, String[] names) throws URISyntaxException, IOException, DavException, HttpException { + CloseableHttpResponse response = null; + + // processMultiStatus() requires knowledge of the actual content location, + // so we have to handle redirections manually and create a new request for the new location + for (int i = context.getRequestConfig().getMaxRedirects(); i > 0; i--) { + // build multi-get XML request + List hrefs = new LinkedList(); + for (String name : names) + hrefs.add(location.resolve(name).getPath()); + DavMultiget multiget = DavMultiget.newRequest(type, hrefs.toArray(new String[0])); + + StringWriter writer = new StringWriter(); + try { + Serializer serializer = new Persister(); + serializer.write(multiget, writer); + } catch (Exception ex) { + Log.e(TAG, "Couldn't create XML multi-get request", ex); + throw new DavException("Couldn't create multi-get request"); + } + + // submit REPORT request + HttpReport report = new HttpReport(location, writer.toString()); + response = httpClient.execute(report, context); + + if (response.getStatusLine().getStatusCode()/100 == 3) { + location = DavRedirectStrategy.getLocation(report, response, context); + Log.i(TAG, "Redirection on REPORT multi-get; trying again at new content URL: " + location); + + // don't forget to throw away the unneeded response content + HttpEntity entity = response.getEntity(); + if (entity != null) { @Cleanup InputStream content = entity.getContent(); } + } else + break; // answer was NOT a redirection, continue + } + if (response == null) + throw new DavNoContentException(); + + try { + checkResponse(response); // will also handle Content-Location + processMultiStatus(response); + } finally { + response.close(); + } + } + + + /* resource operations */ + + public void get(String acceptedType) throws URISyntaxException, IOException, HttpException, DavException { + HttpGet get = new HttpGet(location); + get.addHeader("Accept", acceptedType); + + CloseableHttpResponse response = httpClient.execute(get, context); + try { + checkResponse(response); + + HttpEntity entity = response.getEntity(); + if (entity == null) + throw new DavNoContentException(); + + content = EntityUtils.toByteArray(entity); + } finally { + response.close(); + } + } + + // returns the ETag of the created/updated resource, if available (null otherwise) + public String put(byte[] data, PutMode mode) throws URISyntaxException, IOException, HttpException { + HttpPut put = new HttpPut(location); + put.setEntity(new ByteArrayEntity(data)); + + switch (mode) { + case ADD_DONT_OVERWRITE: + put.addHeader("If-None-Match", "*"); + break; + case UPDATE_DONT_OVERWRITE: + put.addHeader("If-Match", (getETag() != null) ? getETag() : "*"); + break; + } + + if (getContentType() != null) + put.addHeader("Content-Type", getContentType()); + + CloseableHttpResponse response = httpClient.execute(put, context); + try { + checkResponse(response); + + Header eTag = response.getLastHeader("ETag"); + if (eTag != null) + return eTag.getValue(); + } finally { + response.close(); + } + + return null; + } + + public void delete() throws URISyntaxException, IOException, HttpException { + HttpDelete delete = new HttpDelete(location); + + if (getETag() != null) + delete.addHeader("If-Match", getETag()); + + CloseableHttpResponse response = httpClient.execute(delete, context); + try { + checkResponse(response); + } finally { + response.close(); + } + } + + + /* helpers */ + + protected void checkResponse(HttpResponse response) throws HttpException { + checkResponse(response.getStatusLine()); + + // handle Content-Location header (see RFC 4918 5.2 Collection Resources) + Header contentLocationHdr = response.getFirstHeader("Content-Location"); + if (contentLocationHdr != null) { + // Content-Location was set, update location correspondingly + location = location.resolve(contentLocationHdr.getValue()); + Log.d(TAG, "Set Content-Location to " + location); + } + } + + protected static void checkResponse(StatusLine statusLine) throws HttpException { + int code = statusLine.getStatusCode(); + + if (code/100 == 1 || code/100 == 2) // everything OK + return; + + String reason = code + " " + statusLine.getReasonPhrase(); + switch (code) { + case HttpStatus.SC_UNAUTHORIZED: + throw new NotAuthorizedException(reason); + case HttpStatus.SC_NOT_FOUND: + throw new NotFoundException(reason); + case HttpStatus.SC_PRECONDITION_FAILED: + throw new PreconditionFailedException(reason); + default: + throw new HttpException(code, reason); + } + } + + protected void processMultiStatus(HttpResponse response) throws IOException, HttpException, DavException { + if (response.getStatusLine().getStatusCode() != HttpStatus.SC_MULTI_STATUS) + throw new DavNoMultiStatusException(); + + HttpEntity entity = response.getEntity(); + if (entity == null) + throw new DavNoContentException(); + @Cleanup InputStream content = entity.getContent(); + + DavMultistatus multiStatus; + try { + Serializer serializer = new Persister(); + multiStatus = serializer.read(DavMultistatus.class, content, false); + } catch (Exception ex) { + throw new DavException("Couldn't parse Multi-Status response on REPORT multi-get", ex); + } + + if (multiStatus.response == null) // empty response + throw new DavNoContentException(); + + // member list will be built from response + List members = new LinkedList(); + + // iterate through all resources (either ourselves or member) + for (DavResponse singleResponse : multiStatus.response) { + URI href; + try { + href = location.resolve(URIUtils.parseURI(singleResponse.getHref().href)); + } catch(Exception ex) { + Log.w(TAG, "Ignoring illegal member URI in multi-status response", ex); + continue; + } + Log.d(TAG, "Processing multi-status element: " + href); + + // process known properties + HashMap properties = new HashMap(); + List supportedComponents = null; + byte[] data = null; + + for (DavPropstat singlePropstat : singleResponse.getPropstat()) { + StatusLine status = BasicLineParser.parseStatusLine(singlePropstat.status, new BasicLineParser()); + + // ignore information about missing properties etc. + if (status.getStatusCode()/100 != 1 && status.getStatusCode()/100 != 2) + continue; + DavProp prop = singlePropstat.prop; + + if (prop.currentUserPrincipal != null && prop.currentUserPrincipal.getHref() != null) + properties.put(Property.CURRENT_USER_PRINCIPAL, prop.currentUserPrincipal.getHref().href); + + if (prop.currentUserPrivilegeSet != null) { + // privilege info available + boolean mayAll = false, + mayBind = false, + mayUnbind = false, + mayWrite = false, + mayWriteContent = false; + for (DavProp.Privilege privilege : prop.currentUserPrivilegeSet) { + if (privilege.getAll() != null) mayAll = true; + if (privilege.getBind() != null) mayBind = true; + if (privilege.getUnbind() != null) mayUnbind = true; + if (privilege.getWrite() != null) mayWrite = true; + if (privilege.getWriteContent() != null) mayWriteContent = true; + } + if (!mayAll && !mayWrite && !(mayWriteContent && mayBind && mayUnbind)) + properties.put(Property.READ_ONLY, "1"); + } + + if (prop.addressbookHomeSet != null && prop.addressbookHomeSet.getHref() != null) + properties.put(Property.ADDRESSBOOK_HOMESET, URIUtils.ensureTrailingSlash(prop.addressbookHomeSet.getHref().href)); + + if (prop.calendarHomeSet != null && prop.calendarHomeSet.getHref() != null) + properties.put(Property.CALENDAR_HOMESET, URIUtils.ensureTrailingSlash(prop.calendarHomeSet.getHref().href)); + + if (prop.displayname != null) + properties.put(Property.DISPLAY_NAME, prop.displayname.getDisplayName()); + + if (prop.resourcetype != null) { + if (prop.resourcetype.getCollection() != null) { + properties.put(Property.IS_COLLECTION, "1"); + // is a collection, ensure trailing slash + href = URIUtils.ensureTrailingSlash(href); + } + if (prop.resourcetype.getAddressbook() != null) { // CardDAV collection properties + properties.put(Property.IS_ADDRESSBOOK, "1"); + + if (prop.addressbookDescription != null) + properties.put(Property.DESCRIPTION, prop.addressbookDescription.getDescription()); + if (prop.supportedAddressData != null) + for (DavProp.AddressDataType dataType : prop.supportedAddressData) + if ("text/vcard".equalsIgnoreCase(dataType.getContentType())) + // ignore "3.0" as it MUST be supported anyway + if ("4.0".equals(dataType.getVersion())) + properties.put(Property.VCARD_VERSION, VCardVersion.V4_0.getVersion()); + } + if (prop.resourcetype.getCalendar() != null) { // CalDAV collection propertioes + properties.put(Property.IS_CALENDAR, "1"); + + if (prop.calendarDescription != null) + properties.put(Property.DESCRIPTION, prop.calendarDescription.getDescription()); + + if (prop.calendarColor != null) + properties.put(Property.COLOR, prop.calendarColor.getColor()); + + if (prop.calendarTimezone != null) + try { + properties.put(Property.TIMEZONE, Event.TimezoneDefToTzId(prop.calendarTimezone.getTimezone())); + } catch(IllegalArgumentException e) { + } + + if (prop.supportedCalendarComponentSet != null) { + supportedComponents = new LinkedList(); + for (Comp component : prop.supportedCalendarComponentSet) + supportedComponents.add(component.getName()); + } + } + } + + if (prop.getctag != null) + properties.put(Property.CTAG, prop.getctag.getCTag()); + + if (prop.getetag != null) + properties.put(Property.ETAG, prop.getetag.getETag()); + + if (prop.calendarData != null && prop.calendarData.ical != null) + data = prop.calendarData.ical.getBytes(); + else if (prop.addressData != null && prop.addressData.vcard != null) + data = prop.addressData.vcard.getBytes(); + } + + // about which resource is this response? + if (location.equals(href) || URIUtils.ensureTrailingSlash(location).equals(href)) { // about ourselves + this.properties.putAll(properties); + if (supportedComponents != null) + this.supportedComponents = supportedComponents; + this.content = data; + + } else { // about a member + WebDavResource member = new WebDavResource(this, href); + member.properties = properties; + member.supportedComponents = supportedComponents; + member.content = data; + + members.add(member); + } + } + + this.members = members; + } + +} diff --git a/app/src/main/java/ical4j.properties b/app/src/main/java/ical4j.properties new file mode 100644 index 00000000..6db152a4 --- /dev/null +++ b/app/src/main/java/ical4j.properties @@ -0,0 +1,6 @@ + +net.fortuna.ical4j.timezone.update.enabled=false + +ical4j.unfolding.relaxed=true +ical4j.parsing.relaxed=true +ical4j.compatibility.outlook=true diff --git a/app/src/main/res/drawable-hdpi/addressbook.png b/app/src/main/res/drawable-hdpi/addressbook.png new file mode 100644 index 0000000000000000000000000000000000000000..79655934efd520b62f1b1cc08a702d19d2a5a882 GIT binary patch literal 1212 zcmV;t1Vj6YP)0Qsv{h@hdR2lgYQc{R$6Bp!igf8lbm2-xurXR_D(FI7DhjS# zXf_q9MNDH*8*PhN@uNXY`CAl3uvA?65QHqMNu$Y(oAcy$c$3VVkEE12Ffb%{-us_( zfA^ew4s@(z9qX8xT@p@gFtc4|*1M996Fgu@DgyfKjWCBLuR%W7^xBY zLWEjsUBDvns-$06@(^#2#2cL7g39$Aq;A076V;Z(|tPY9t8cmlWvn36PMX7@Pr zREe(u{lH=1AYi~i)lQV%u{%Quzet)fGm<1}tpic!*$M0f-jnp9M}F9&S&c})pN&IF zlFWt>egp2$Sl4iND&`SyX6t~5fP<2zT>O*HakZ!(;IO23{TVm-&Q&tjHjGm-XWD!m z_*Bvn$7aM2Vl_5>z!dONB!1M-&4N4ElaWMnXQx6Agf;_1lD_cBkJP}X&#_4(zY%y2 zSnoU43w#Zn_A5qdiR|>+a(Aavj@*A<(wPXGLJe%DB)uO+elKtp@UM$M0lXvW_YlH7 zaBIdZ7u|{NA%xROlALQgjY^s`vwIuB=1MPi>wuHM0Z9v9OYU2OjDFxtAZZw(ls{*& zS!}+OH0J*w1fB#9?{)sbx`qsZ@ZZ06HD5^x}GJ%MLk{A@-L zyMZIXL_N(dcZ0fE9A%u;<-5K|mYk@yP2y?(b;EJeg~lBDd|yx@`lCj*-v zuk7O?1VRY8Cl*~aW<|f(%m5p_NcH5vrr_`X1ek1Q)Y7N}i!NflM|J@CHUpdMGO#Il zxAwNAj8?;R~=?smWZ)UtR=G0iQ{l_OzFU)0!IM zQ*4g6dceD!1A~#k%Yi{ND|o@GEO9Q{icKSDpyD&meZV;%)>i7k^8##IExz78BCJekgCPMMQ1@Pk`ITm|bh_R}mo$!%X)p^eF5ZSK)%!Fb1F>OK@GN909 z;Bk!mKdiOSV{)(5$Tp6F*VfuE1EG+Jw5hWOl;e&Okzz!)*uhz4U`<3SX=oIr;XW`6 zTyo?gNb!3~1;(NNPkH3f%*$J<4V;G=ZwV>;FVRJv9X^#>!?= z)PY42nTkZ#8KneGs|c(EBPqQf#R#x2B5#0(k0F7t_ko&vKP%k0v+ z*Q=}+dqkiFJW@k_j^2mBXJgDcW6Y=~{&n@NYjHUMt{7w9hhf-ET1IavZ+$&j1)jz3 zRT4C~N!8(Y?4_3YFgM*`mdH?_b07*qoM6N<$ Ef=HizN&o-= literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/calendar.png b/app/src/main/res/drawable-hdpi/calendar.png new file mode 100644 index 0000000000000000000000000000000000000000..3b5f92c35825f6bc3193d8b688a3bc482a6bb958 GIT binary patch literal 1429 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWuD@%qp275hW46K32*3xq68pHF_1f1wh>l3^w)^1&PVosU-?Y zsp*+{wo31J?^jaDOtDo8H}y5}EpSfF$n>ZxN)4{^3rViZPPR-@vbR&PsjvbXkegbP zs8ErclUHn2VXFi-*9yo63F|8hm3bwJ6}oxF$}kgLQj3#|G7CyF^YauyCMG83 zmzLNn0bL65LT&-v*t}wBFaZNhzap_f-%!s00+w{G(#^lGsVip)Y8@5)!e|;$-u?X(ACh=#L&{!#L3yw z&D7A`#K{n**Cju>G&eP`1g19ytk=!b*vY~Xr(RHE$SnZc?2=lPS(cjOR+OKs0QR(1 zCQi4Qx#Bbrsy79zTR{FWbHk}uALtl;P}CwtHB1PYen3ok!Ub~RNk26YnBI$k3Hw!J zm=gm76T7F2V@SoVH<8g8WQw z>P?2^rIq)q_I_h-F#Ed1L##@roo2M@kDS)a{@>IVPYc#$`nLO$%d-_S$&a*of{OlB z=e_yQ-VmLt?-Dz0Me$!t2N!F`S&3(TPJiq^!ss8m=vSPgV4d@AW0j*qv#(#VlWMRv zT4lg6*WnHSqy3qg^*oFVymK=on^y=wW>LAdo+ti-z^kCyOCSCZ`qp{WUVuSTFT4MT z?h3BLtIu-oWP5G8z01y5>&oO?CRfYdPIwdrPIjA}m?8Qqi2v1ewu73T^;7mv+{ka3 zx`p=*my6gFv#29n3k}O|Epz(iyZRjKd4)B6vu)JaW8T>?DHiAdSbCb$S zK~!ko?U~POTvZguKl74F6H~BFp*RO>5W4A7FIbAjWhk^1b>${VtxKVQfd7QxA5ca2 z4Mm}3S%Z?mX`xiI&9$_Z$zP_2H!aFd`o5OwQ+;i{u{2ZvM zrkZN1=~bW*!dTW8QDYRFcK818Dd0Tt4$uUqwX_X%fP28IyYCEo|IiEwA>=oG3}^vY zfs4Qvurok*510aGfED025Z!%yzs#{3uy6C{fDeJQK&B;s1J8lSz+d8>08Z1KtHD1eK@2x4<8|g$}T*<&%QcEN})`1m*;-Jz&kqH-S?zMvpO)mp3v92{so6iSL0Wckdiz@IgHBo4^Oa6+v#L z44fuXPG2W-4){pyb>JJIGwifO2-9XZZ)S65w&m`-gXEi87x)!;OMYQy_Ir%+&y-sz zG{DR-vtz)gQt(2s*&HT3gpdIr1D^oz1FxCc!x&?4kg+kwu9>X^7Zea$g4bS=0a=j& zy7rbr<+L33S;1>f;0-}ySI^A?JSXMaBq_eQ>Xe9W+6!4uA)K{KxEV+=|kU_oF&=~&R@#o4Ji$#V{COGwxjZ3vqU91rx-u4=hWhGQ@VDfo2mvh z1+^zYr$nvN8}eKfFgnuJD02K%1NtTL^IUB+PTG~OyzWW{90es;2q^}%b0zWQA#8pU zczvJlN(OX<;nP5Kl0)PS#uwK(dj!ho`6QBJ(#?%PGWvVP}1v4 zXJPq~C7w)F1KtKMg%D0AI)6JiQYq4NQY~g%x?;*rXNk_W4ct->d>ptQLO3WQ^?)Vd z3w@S`W#bS+zbm^g*?XE)Y>#{@k=jz0lv$~z%*r^J1h0yH1-Q|N)60w&_3jPH>H$CZ zH=immX)2MQ5}4To;1pnTxC>_1bN5I4`{+Tvbxz)bVAIl)pMbApjNLM0m2nKVHdGS7 zT7#_;qs1*%lTUJ0Wq!0cExkuKTKr!RcZWE4zJD$nudn4dosz5$g^ubEj+$z!sivAL Z(La80+4kw-k9hz9002ovPDHLkV1mw7^E3be literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_action_new_account.png b/app/src/main/res/drawable-hdpi/ic_action_new_account.png new file mode 100644 index 0000000000000000000000000000000000000000..9a41829f6dc9044443dbac1e670b889d87f782dd GIT binary patch literal 962 zcmV;z13mnSP)ztmYI&Mbp%#NzTFV;5d@By?1F6$RBP|ooJ-|m`LPS2K5kyZ)V@D`hlaK=Y0_o-x!1M z`z@Ey6<}MP+f`s5_yKGOcI)>A;IfGPY1s-W`4M2G&TSWXEh6K-?@xK2_XOCZE7b>N zJkOi8SpnCQd=@y|V4O|B?TFmUz$CD&`Edxy*|?Hxxd0o1y%EjFv7|{IP1YfGr2rTN z25b^JV;MkJFB$}J+!*5n?|_5AvDo`_HUu&>AN{~tprpFbM(owGH4@tZmem>?nL!X+ zsUY_I1sejcMFIo-Rf|kAjsp~d*4U`;`}n?J@jNdZCTfCrKLw^d&r4j)T#MPq7&8Ej zCLE^#JQR_@#)aG@2N&TZZwlXHexL#Kn@WuQR^XC|yjksJqy^VPHesLdgydI~kZF={7(flT8IF)I z0(syQa3dtY7IJBA{G9}{ae>FcUA51}q_b*_ae>po05B;cGpQx9o+iX)Q_`T9fTD;5 zDdgaO-Kou`?LzzXDnwp30en>n1u4lm(3K3DVWaU1`&~rx?Q|ZMSz&qINTM%kL24^B zveDmKIcI=r;AVIX(kTJF2d)9v^?IJf#~TGKB9(~t%Sm0J=`hBO^8cu@8cSYRyV+i? zD?e*+D(UJMTeT-fi{MC|SuSW`VIVU=E=;F0=#P(#9}w2(ajW^2g0+GyXL zSqJ%`)|;l|!dZCUDz)=SpR10nXsYJ>s0~aC( k0-b}7{42JLF4_?P0H#(vhRG5TmH+?%07*qoM6N<$g0B;%*8l(j literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_action_new_event.png b/app/src/main/res/drawable-hdpi/ic_action_new_event.png new file mode 100644 index 0000000000000000000000000000000000000000..5a5d55878ddcfa53078e18f26f9b1432bc1eaee3 GIT binary patch literal 409 zcmV;K0cQS*P)e(6rKz{t}7`Tx&vf&c&jz$HR`l4V&! zk9cOiXqra&L?HT^=QPrp^)-#kCjtlVDa4*Yv~vB4>LfMnLrHbj*&=XM!z5yx5qQ#g z7;;<+Mq)dKq>Q#Eyr)#`QN$w=D7BnSRG2&U{Zj*5i`Y~IM+xX^K#mTt{a}>7-3{2h zx|GqH^*KP1s8|!A6uNCSNSjc&Orex@1PBH98fbkZfOeW&4eSbarOnrkLnO`&#;UEDaLnF9e3fQ%Z&%g1{+006)=-U18&KiW*HU+T?Z00000NkvXXu0mjf D!qTR` literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..dce11e1ceec99ed1361a59de5dcfc8547750b31f GIT binary patch literal 5274 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/ic_read_only.png b/app/src/main/res/drawable-hdpi/ic_read_only.png new file mode 100644 index 0000000000000000000000000000000000000000..aca46301e9644cd1c78dc5eae5c4c3a7220bc4a3 GIT binary patch literal 1438 zcmV;P1!4M$P)2;5i=~v_D4dD3^a7PqE*%(@gs2oGHr%8jpkSgV`eJ-CA(1y9NPJQs zcoKsqctH^nFik)npenNT&`_YY^ujH}04E;vpaAc0^ zng0rV97cZj8(ULYehy2S61ChxirJAsBCV5ZDxHp>OiJszsmd z7F`&mp^(8Yt1lsdBh-9XC?6ZOcJrb~o+0{J!ekbRQjXKeZ1R~a?#PRxjA^9vnQR}! zDjo}EeyuH|*qyUdJ8yP?o+iqLpS$vs_=#EMA*iH@Mi!Duv1p`~dKx(HH4R^)G}OLl z2{YWisMngzPVp*lu#|FfPxe^9iwM;eQp!m>SzvgguCkasg#1+$QOba5rb!MYaZ?Y(-`Y3KW7aSWWww2re!xayhA0r{ z_ZJ2&Lp&%~pq)OCgGNR)lDf{VCLg zm~-(XHB`7}Q$dg(=e(AP9IHi~Wg~+kK%3X%QU(BXs7O@|Ws(D+DATw@zr$jhXqUG{ zK0(gNp%g1x%&Fb1EVH|mA|>1Xqm+Bx2OynX(ZnU>=&el|K!1%|!bI$AO0JUQuuzo} z`nd<#sDk|5cvVn$6C! z0hELcCQ!vwEToF)1WZ?JmFaG8fcn5M<7S}OiIb`q)lU$X2!32 z2?6wMwo=M`%Ec*qv8jmSsSvVXS8F^@yGlD@ULDUS7crFxK?Nm^CZC_$W>L)92$L^= z6Akia95Wx(_Or;nf%sXW$>`f)6azgCebkE~18AicVvC`}BE%+-P}v+Z)+S^i^4Q>6 ze+F6XXD=t@l_XW~09Ed7FpWB`S8F!ozHnwSkszQF;y&l(zSJHefk(zh4ztqfPM{D4 zJB}CWyYQ1vKNkqd=JCDKj2wPo1rTOO;N!Svr`PfhJ$y?WebJYv6d|(M%R1f+e3{5U s-_}?ByCl3^w)^1&PVosU-?Y zsp*+{wo31J?^jaDOtDo8H}y5}EpSfF$n>ZxN)4{^3rViZPPR-@vbR&PsjvbXkegbP zs8ErclUHn2VXFi-*9yo63F|8hm3bwJ6}oxF$}kgLQj3#|G7CyF^YauyCMG83 zmzLNn0bL65LT&-v*t}wBFaZNhzap_f-%!s00+w{G(#^lGsVi#&Ct}+$imdr$-u?X(ACh=#L&{!#L3yw z&D7A`#K{n**Cju>G&eP`1g19yq1OzjUQlAlEdbi=l3J8mmYU*Ll%J~r_Ow+d7PlC? zn3-5OIXVK(v%u~ah~5<3ZZW~>7JZ;&^g&UJ6xA>xVEO?u;RzSWfhYacJYae+0w(OB zPunsW7#L@Hx;TbZ+F}XRH$BVJ_~tbB>ux&w{i&Jpl6k3-y`NvKHk21q zV1%GG?pIzqOj6l*_t-(U9qjp;vo`HwN=}ep)5vUa_N7?Rr}hN4ng+Fwoh&_BpW+wD z&uL8OV?3O|erD1ih8>LjGWVRk>cn-L{3q>ay)F~Cc;}xn`8m#J@(k z@^-_wj*fK>KI!`&dhL6Y6 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/navigation_forward.png b/app/src/main/res/drawable-hdpi/navigation_forward.png new file mode 100644 index 0000000000000000000000000000000000000000..812b3aaf48b3b140a17343d317d2ef7985735e62 GIT binary patch literal 1304 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWuD@%qp275hW46K32*3xq68pHF_1f1wh>l3^w)^1&PVosU-?Y zsp*+{wo31J?^jaDOtDo8H}y5}EpSfF$n>ZxN)4{^3rViZPPR-@vbR&PsjvbXkegbP zs8ErclUHn2VXFi-*9yo63F|8hm3bwJ6}oxF$}kgLQj3#|G7CyF^YauyCMG83 zmzLNn0bL65LT&-v*t}wBFaZNhzap_f-%!s00+w{G(#^lGsVit(Am`4(%9M5$-u?X(ACh=#L&{!#L3yw z&D7A`#K{n**Cju>G&eP`1g19yq1OecUQlAlEdbi=l3J8mmYU*Ll%J~r_Ow+dZnrq% zG!Lpb1-DzAaO%|uIz}H9wMbD769T3m5EGtofgE_!Pt60S_ab1z77){EW?*3K@^o@a_5?y~}!)#fxe&fZt$}{a4q5e zyjq9j8_%9cUzr5#cPxvt-pKeuK#U=TLz_XWMU{c|kd%Y$A@&BX7X1dUMFO!*iw;z2 zcc?X(w#dFd`QNvM|8k}{kgI*sENDvLx@i}#u3RAMB79(1hOq4{0mZc9Y|$f<=Yr0k z{^_9nRr8lY#=_p6vu_^$J=--p!$XcXQP<{&O|d_I1|kfaCUQXf`3V0!J--I?+;AN zXlPjqVkn<8SDsUuH~B!_9o^TAx+bYJ^tF5c3%fUP2rw!g2x9)jX~1wNq4KVD(EUxI OvdYud&t;ucLK6U1mCcF( literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/show_sync_settings.png b/app/src/main/res/drawable-hdpi/show_sync_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..4a6fb5b5982798b4183b93efc21e26f94a57a2ea GIT binary patch literal 1550 zcmV+p2J!icP)?1)E7k zK~!ko?U`$6990y@e|L7%ST}vQwoV@?A`ua7ayKEZ4+J3}M8Ei55Uhx3OCO4eTC`}f zwc1uh`auh#g@Ooz6~PYy3$4xWoW)wJ6-=w9&DJKZt)@+r?C$l0XX?0g^VrElF$abu zcV_PV&%OWiKj)snlug-`jk#HCM>O`Y16>Z%TAQ-griObij+wCnFCtiLrvZzBhk&NY zBT3+EU?1>`uU{G?OpRbYOF+a;hAI0q~Mmgsmd@REr9F=;!X;8y}G zE8O;g??hxZYITFqnw;D}WL7TO}G6hJPG=3l=8Eb8=7H!w7W8FZfh+K@$H=zfbGTIc&z|R9- z6p>>iU5s*TY;45yJc1w?I|>a$o3PF|A^1uG86EdLFC442)^4`e?z7fz$>nkcK|ng4 zzVd8yb940$Tt#h`MZjMIx`30wdm;F%t!!i-4NGcD9^AxBGM%y=SM1o>2w+q zISzcP8Mq&K7T6BV2JQw@BJvurMo)1G;FWAPdny{#qjexuneZCWgfyze7YWKGnmcv@ z9s2W0;C|pKYwbJ6m`-ENYGArCCKY|ZlEMu|q2D!g_A6b!3fmyVt$uTJGnq^#JTs?& z2H*}=t}lUYz;#-R9|T%~WzM+`#bR-DB9ZvnIrp%&HffB>l}e>cB2pTy11EqTkz_SE z($t^HWWrz9=`pfackxHf&I^GaW6Y)?2s*8``+=8%*4oFFVt%Y{mmaL&D{tG`~A zXM?VMpYBpEuu-Y|F7OHPp@BemIG!g=x%NJUe*LRPrG)#zy%%qLPXvk&(`8d zj-UlV6zBzG%nRD+I;tJ*rNCU^o9^!JJB=|LH7+gynUMbz8pIXK>2xBIIGV|1`V>^k zIro+Zc6TzFT+`m(KB)KR8)Lj|HhX5c#5obI#n6cW2+snPN+qMoaoS&{|KG)b5ClC# zDbIulaRsN0_LOb_PKihx|79EN|6Wenl>J}qPf43dyh|hhZ~y=R07*qoM6N<$f@ML- A>i_@% literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-hdpi/view_website.png b/app/src/main/res/drawable-hdpi/view_website.png new file mode 100644 index 0000000000000000000000000000000000000000..9edbb1b585e93e9ab662c42455f490bc98809b56 GIT binary patch literal 1917 zcmV-@2ZH#CP)UMrt`dh`5&`AIcM$} zr-deVC6l@L+_U#ykN?1R)^z5u>`gFY8jyhJ#w7aL@Yrr7zE}##n>!*2O z6nF&~cK5}yJv8_ewM*+O?; z0H)pjge)U0W(XPzeMK#Dex3<0q{I<(%tU@eg&M=(pDnOe>7@l z@0A5ossqR6H1fu&30guob)+h8n_;KLYRNu%yt3&!1sjn2CyfA5N5Us zxCJ;b$NT|z|1R(wL2?tYJ&ANHvcMMLIx+oo;6Y%*-GAop_W&;d8zo#rpXU`I{+jG_ zEpWRSbiLaJ{KMVvHM6?Aj{v`xUtFiN|2H2H?wi0D#LR>4J{)75jxo-Qsg1y=1(igk{oiJ0n}N?us5CO|(b;BZ z&9dCEln<;^lzH0S4;G~PyMYJ9=->5wO9y8;qC;>1Gco`|m z()AMf)U@!Z!em~^E?}#fJq3)p`=Xik0yhfJ4~fBrlE7i^{*$s2+swLUsd43p0W+(m zRcxsMqQRsvJ5?Zg3wTLBkkOE}NVRT1^4w_lNnvh;a7gtABh4~Jx`9le0$(R?dRWLZ5`00GxM3%s0 zR10T;FY4I^Vq`>{4dgQHQq%?9xteU;0zqNwTH-Rm{ z<8sE=fLDQkNr*B?wu+p*tUycZt`XqfvcPbqrap17JCfR-oVK&xU^LmISB=kL0ka+Q zmnVULN^w)}ex!{h0`N89MR)&kSY~==I86N|xulASCjF!rnhn2{R@I58i{FlkyeGa$_IG~K$m9xgY)=nuw zZOk|K2?CmGTgSx9@){Zqn!G@O4i#7@Qw%3U9Ci15fgb^PO7msZ1e&>UntlA75~REP z__7-s(U3HicgTx?x93fomKp9oT_Mb#r#WhVa&Z~(9wA_8sgYx?O(+`&_7%)=OI_(5XO807s08bOOEMEpu=$UmSq>Yrd`rwND>L5LPHQDdW4 zCWS!}3=2UEvG8I*@S})WsDW6Ci18ykHYe~Hc6MerK?vl*!Z3T^oO|v$_r4ieUu$W3 z?`vNFS8(tB9`Aj(_dZJ8Z|b>)CK4G1V&DrfWoEC#B+aa$=N0p^pP!${dw&wx3H${* zfiCBqIp>C*b05v@>vHU>CLr(*;Dp$B05iaC;3BXI=yA?{Xf%na0plG4+JG`J4crm< zW}qrL2fX*Y%q+Jj3OOa{1g;AF0Wbwz67x9mycoDwvdN z=a&L61->Z)BLY7P^o#Y3Ou8@OuRiD8J2P9zErFT+20jCq6U+-@J}c(O)QT^FyTB(6 zDgrqonK;`6!EEmY2E;rLG#@duIWa%1q$*}M4s2J4vE$;-T61(ItCH~{njEx^+h<^bD(n`Snh_uU|?0!96n z6uV$%w`8~^^CB?T2zXWnB4AdN@SuR&y!RJ?_8QD@m|3OKx1v(Y2^j%Dbq4$ZcI$+z z0$ssS3Yk}$c_mBhf=I*Uluowxl>JRJ&ypZgCH5+2F(qznCE#fZRDlIaP-|kw+V>t< i5%{WpsI0FwyZ!-b6hdpa9^I?}0000IpC5VU(4sM=aCJBmul>Suss;LMKZflXDtc+do_!Wyt+K|`DYpMj35 zj(-Fqf;nyi3lWnApsA{bMp1@@TR_jcSF=^Iv#p}eo)xKHL_PuUEZAS*J8%NL1ahG2 zoNK7+PZ7a;Kb{1<_qW7bw>uT!kE(w4-k&(Xhm$UfNfR1?iZsvB*9H1Rp-um&mN?`pp(6bO>RDp3Xi~Kw-^b2 zi%Z~_t;;?(9bnOXr6mwZXC0UxFuGG!Jyg|0;B>$^r_EPXmRTR!wSbi&vda?5+yz!d zq!p88B~tx!U>A6sAhmPbH-P#O`+vYb@IOo>yIZ}nc{V|2b>O|NdV>8daIXt76}$jm z4l3^w)^1&PVosU-?Y zsp*+{wo31J?^jaDOtDo8H}y5}EpSfF$n>ZxN)4{^3rViZPPR-@vbR&PsjvbXkegbP zs8ErclUHn2VXFi-*9yo63F|8hm3bwJ6}oxF$}kgLQj3#|G7CyF^YauyCMG83 zmzLNn0bL65LT&-v*t}wBFaZNhzap_f-%!s00+w{G(#^lGsVip)Xmb^$->3d$-u?X(ACh=#L&{!#L3yw z&D7A`#K{n**Cju>G&eP`1g19yq1P3sUQlAlEdbi=l3J8mmYU*Ll%J~r_Ow+dZnrq$ zG!Lpb1-DzAaq86vIz}H9wMbD769T3m5EGtofgE_!Pt60S_ab1z*0?Ksm4Sirv8Rh; zNX4x;(`~&E2Z*#y{uQ{fiEq&YzITiG%7od~&DGV#3j@smH_qPgAh2<+OF>alf#!{a zt{a*b#YVHcZU}pu@OMFOjsBiS=NG1#)vDh7Hc#eFe)8$rykh$#>%QdHOhN6GY;KNN zhxEM(HgfC5;~4)w5L;sU_iVFg02BMY*)x6AW-#2)kYIi%(%BTrCBYy+>-)0Er9oYUEL!?F>my5k0FDW)<&Ga5_*scaGW-recm?FIM}$>a$M! ze0$^ZWwp<019NOnDzv7rVvC74)GM<+y@g@wapP_GE3W=*{V%X!+0}LG&z?(L3WxE^ z{atmo=fD(=ZDGE$mpAot>@ceNarV`(Ew}flFVdQ I&MBb@0Obk&82|tP literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/extra_actions_about.png b/app/src/main/res/drawable-mdpi/extra_actions_about.png new file mode 100644 index 0000000000000000000000000000000000000000..25d68263f402556fa22d663722e81f82ef41c481 GIT binary patch literal 671 zcmV;Q0$}}#P)4lD#gEc^o$ZAB}^`Wi`L zcm3I1BjE!JArR1z6Ltwii8<03BsOnxId=B;mNQt~2LsFQ%=^BbdGCD#3oI}ry!WNP zg-|IeVijluHGB077)ts!cLCmeB;mbZ23kNDSOdNrF(%|1FamnOo}|-cZD|21@^#=Y za07^dcfb!|Y*Xt%6W9VAZ~(lQv@=aar2zSD;4$zM=t~+*5#0ecfvdnPNuTpXEQJEa zah&}5Bj79WLej4Ho;Z&4MDIK2J_5IZ4d+}WDJruA%fJ&m-)EAJ(|_Wl+A(CoNEq8%P(qNL@<{Sba zY#gS0KuW~GlD1n|fGu9NQrk}6P0q2SIk!;@%)w%;SDcQI04?+ zILRPym?<4ttWtKo4m>b3dL3xV1jru(H-Wd3c0wFWQ|vf$&V2zkfIH5)GfDeX9JB-6 z2d)DzC4I_s(6SejZR>FV?n3fkT*)rtQaaab`2q`+!5@hj7xbvI8I%A3002ovPDHLk FV1g1m7*+rP literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_action_new_account.png b/app/src/main/res/drawable-mdpi/ic_action_new_account.png new file mode 100644 index 0000000000000000000000000000000000000000..10e17d08fa5f1630badcc4b379e77f4d5e4c1111 GIT binary patch literal 666 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/show_sync_settings.png b/app/src/main/res/drawable-mdpi/show_sync_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..9e7a77563e145b6b44359435b20d86e50b5f2503 GIT binary patch literal 910 zcmV;919AL`P)Z{%Nt=6|L|Mg${ga0x_ z5s{+bqfWeXB34yIq!L&Jv;nitMh|cic%rJ=f^HnC7=qJBfqLL4;F|+gzz(2QMA}s~ z9oJa0%;*mQ(}BDlC4gSwKJd*lGT%A(&^Z_QzF*RIB8~%gc|9-#2tr4bfRn0v4Y&Y& z0vs?aZV3`)q<$JuJ!tP6u+Y9I0OPEz)8ZN}2Ls6>pv?kXMPw;36{sXC4uKUDj$ zR054wn!?J&HCj#y-UDAE&ce9JneMPVumq?pp)&?}27HXmjukQ6Z!@p~7zp7ZKPd8Q zYsU^%4TgmWeBVbzRsvh>@`B+92EYOkKLEE>^_p`oAE*C+OT`h92S!4NRJF_W3TV$3 z>-#vAoQx6qj}Xw>Rkce*@O>ZO_XnJF1K~?VWUh1WvU9FpRUe0)DVCW;u@P&Lw;A96 z0L}x)fUdz&S*A9L$ST{a0{VbjvsEJ^J58^22}EQuu+J_|8`ob@)l9LJipWIZtg&DZ zFb8-JOaZ!qdB9BIjH+HQSpw%=GqAv%yKF1xoGV&S-}n2SbKij#K&NFY3EVaZwp##~ z;_^Tt?OEWfs!~`{e?-RpFM%6C2e1t|ZROYl)HF3UB>|e7n@g6U)(GdjO}8Kl$s)2I zSPe7)_kdY84^9AUfZd5iqBWn-p96Y5&%2sRr8*;t$ZFhYAd1sQbynX>>yjGav#O>= zWdE kRlEP$b8XZ=+5VdR0ujSAgp{^u?f?J)07*qoM6N<$f@#^2L;wH) literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/view_website.png b/app/src/main/res/drawable-mdpi/view_website.png new file mode 100644 index 0000000000000000000000000000000000000000..7a27476bffcd95139be3c192ba27fe468c005475 GIT binary patch literal 1144 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 aE*_x%^A9#2=*;5*cB#UnlqW)y^Zx-0G=Vz+ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/extra_actions_about.png b/app/src/main/res/drawable-xhdpi/extra_actions_about.png new file mode 100644 index 0000000000000000000000000000000000000000..69cacb79da36f6b2414cfa2406221ef42c6808a6 GIT binary patch literal 1455 zcmV;g1yK5lP)Ku;1w2Va zK~#9!?V3SoTvZf?zxQS)CJ>ueq6s(Djgo-fc{dijh=im8cM8egLig_MLQ7i2?%W4- z=el|93^8qz#|~R79lO=-p#QfUkhHb_6)5tppT- zE`4+ds45XeWn`w{`q@?^H@S!SP*LA!JEQrW_TxYh=c}@c3N=i*-t^n6XWW7}q zf|Qj1NN2|>e;b$+k*%bjd>;wDz%+2)BcTdhYn6mTQUanVa%O+coB2Rz{R?BvP83B? zpH0%ggE3}DMD7@4x`9_bqAwU@mPF)9TP?s^3(yUG02ojC&qZW8wJ&Nb`oA?8dl|T;W7buMF!c%n(ljAm+a{YVHr@5T86@TZ9ECR~zN)dHRaZivWAvwaj}THQG>&VK}K zCS3b3sLZXFp)%D} z#!Q@G(etHzT^U{v@M|IEtyb*mrlV$PZxipQYWCxdvC3S2w3xU>B@?L zPDa>{7c08;xih?>4kBQPe-B%Cyf8g=0;(PXXF7<0J};)IWbLiDRCp_i1ff-jOPl{HH zqln0sF-Cw3+TSklGH~7)6Gl<=lY6$=SvsIRehGM$XUpTiA6a?)0LRqmg2WH(9imp6 zi0zNZ1&ISlXmmy95qk%it9p#J=H>Ip^NP$NL^Qf2RZvp?AD5&KA)(c4ay5+r$&5H47QqWjkEd%o|S zbI(0RLyjCda^%R7D2y?YK1%{#(uPZFU5^1D00GdHG~a1l<_TySV~i;Pp8^MgFM%z< zN@!>R{0000Je1T-f(S1m<;Q`qfgM1x&G0Y^mWVmvx}^KANhrEhzFMtfjM)L4vFf*! z?uOQPW57P&_y3jjPi8&97&8G(+usS^bA2|vYeq8NV#n3iFG%HxUH5De1=!_g}N*0F;66 zB;Cz~2N+|fdSv=AMuFeE6hyZ}>uUh}GZO;9UK`I*+<$k6@B!ErN}K`ki7{qV>IpE$ zlz{Pw!<)bXV@x@$zA@$~@KM{&QL(Rd*8#ro7lEk(NEipc@O{4qjQGBP1US?sgl<)< z)rC0MTS~#%XvG#u-`J$Q(#7~7VqRHy0-C_`fcFFu?x_Pysm}qD0^pg0x$D4UR(63K zc8iO1osT3fr=9?NdV2sojwCP#-AnCQ(X$T8GkG$;{j^+ ze9&;w7brO}{sgcPlQ@gOB}oledvL*l@^KhHYIDeGN%Jlnre1=|H?2EwwWqrv6eeD% z2&&l=;%`Y*VO~NpU$)JWhrk4@y}>=eHtU5ifakzH;H9Lcq&mZ1Ldsv3G@E3*+gT16 zFy_l?Hhy3a*htC`OhAN~FR-?ccQ)eMX}e@c4l()hUMF2xk{BdqrC z=i**qXugku=PhU1Qh!0NW~UZDr*IZ*G>m7al4IRIH{c}|T00000 z6pD$v&~@EHrlsfpkG}8sITKKyS$mQ(_uSv)ZUGOaOQ3iH>VNK^iO-03Sn)SQY6QGQ zzOYv$keCI0$ruBl(S+qTRRXplbm`UV)-4P&~9$m+# zV*U85d8^I$KzYDAv~NKIH;4kp{MUibbx587G0|_ChGH#1wErm6G?WEcf|HiOR7HoI zJHc4ig%iXH7VZS81t0(?m@gz3X;rV_bj=$@o%sct2b^d7JzNu&kidoix3&z9i6$ii z7IqK?N|V56(F90%*^mek1)?W8J`ETBVA6O*sL?ikvq*$4ZMe)l{x`mCg9I|MfRjhR zLDvGNKk-*1<>)Rj+#Q$e{ z7yQ#q?=s*tQ*byjs1!`3Coq~EIZe0o&ZdG;A)?`!QWhLfO(+HJ-AD1*_Iuv8n<9h& zARq+Zr&s9JN$31K2yzhx)e2Y%1*LMCUrM$1r;yHEc0M+)xKJqzQmRgf!hIWV#RHsx z07QVUf=@WsL> z!oemQ6Kp0(DhIe}Pa6Qi2zX4IO-4{k4FL}t z1I*HZHqgBycAgJ>hAMN=yyx8GGV|h3#ry6}-naIgF`gz!fXj^VEtS{LY%#%Ps9}O; zV=NVPVRBU9rO1cKpc6&%O)XHr>o-r8w$%4&6oe3u3G9=32ts`)tx7(mgOxcXh>V+> zF)ox6!vs0d20K}9d*D0X;dq~p@}0n83?U92R>s;oez{!64*xrmt{3M7#PQlV{`Ew; z#*xNwM08H8jjBpB^Ns0j>?aj z**h6zM*>uy5SL^bcwgEH1Be0PsEm*$nt_|`(A(<3a>g1!%Th1jzo-j1LDZBt;2R%) z3gJ*JKQKD{RF{9iVIda}y{5?kq5i4{IK)(GK1qitOkw7LSizTpHznTC07YAf(ubm9 zT)T7=MXYmjh)A2&&0UY%uYMGvKxBFx(6FYInheNH!w9Kn#%6jPQ=$yuWfLTQ%H@vS zPZmv(K$U@&waBh%zOX0)qzVxeBLU-wHl* zF@uGgS-yLns%rx!1eF(MfDs81FZTD>T|69>BOdWdQ9th-EUH*<=8B{|5v%jeGDSEP z<--M>5Y{f-f*UWn2X{U3QBazH1?5FSS{SMeyxzc0*}!(B@+{0SLQxU_8;Ln39L<*^ zkrQ|<{A8gh`xIg5+J%?k0}VHVQvZQqo@qNy5Pc(UN?E`OV&l?l@OSV3C4RZ>GkH;? zpvE|c228~V2rwPS3UQv(BoUjT+UWMcPwpn`_Idb zbCD+kWJcH%bK;x766OgvQ~H?+A~WJZ4x$uYLruJYdlPFa!qr<@FV<~4axlUMzRoD< z{W0LBIyNrX2Co2i@%2p$^PfD$Am@O@Gl1D-00qU*w|xqGS{?<{GJ88f$2+JucC%@K zCbjv(&-`snCNQa2O^^f?(P5K~ke`4f@DpmpX95w-@EikJg3fVr=XPXk5OfN%w)xHT zU&1Sg?pI8yRSPIHg@UG(4K?hy>^So|-CWGj`_ZBz0l1i$xG5Qa!sjKTAkQDj`F}t# z=5Y7RNV}5~P{KT@pI%=S6lc|4Wq|*{E8oELZ{6*g9*Fyu`%$jQXM${;%V)!<=8UsN zMUDrq&IHXd%lBg3pTrv@^5K4gN{j%Sx?e_9*GryVHWo;(a_;))4GZ2c;pF1sJ>SEV zd+$=sCBTCK!@5vvnjpgo>kBgmhE3{WF(WfENpj!3KNWmtmL$l{D2S(>*||R-Ipr)d zLZqGbMU*3zIUriSOEY+O|F5uh*LOKS{1K3E>ewCT!`ILN^}7rj>`bs|12m8`lR(G8 zaSdoF=T6W;>D(~ll?8BuvQN+>A^?P7bRdP!=1~neijv9*Dr&-@lwi1b0zEC`evK!^ z(UJfvmlVoV1gHZ_DcbgqfVynKP zzc_}gzrF;70I4L!Grv8i)W_mMT=%V|9x#LgJ|O+aCh+2a^(t+n@cbi!{Q3LM zaMRG93y7p#^X?(*^<76tkxaNCgkWB646#x*SvESD#&AzcGlvdN92z(Js%a=x=DGj) zHr)Nf=fO;!Zh~bK3dxMhle1()UC0Mh5gBAz`o)0A5t^DeHeDL*n!wISyJ1UBcejos zF)(Spj#8j?->`R0fMDV35~IzD!(;p*4a;I?FWXDo^%lk z2T;4J6oDW?CM`^B$Kg@QpEx*icm!@nHO{pwqX+~EccflO>s`a+ed@>llC38oeA^4y zxnJkyf>gX6=9z_u_DzsCV*!2+RpaN`?4r#CvG!85wDjyUqglt{gy#c%U(nk=j-eiP zKWn5YsC;)b6s`M)K?p%O5NrNTdJ6Bp>(qfiYv;nl-Vk=9G(+#WWk3$aVfY zKt7CJ5iWH#4S-N9sG*}(QwXd#ZQ^fgO0>jW?vDG`$ z$e;VxueC7tP=XuCV~q)=E)XJEusmi26vn)XI&a}K<#S0x0O)u-F3mtWI6P1_01~jQ z&T!H)@K7dTBYmZ;OELhY#$9C_$)O!;I2$Z*(6DJ9=GPY~(uujYfQm&Xt)P*><2Qbh}NhYKk%fl=oLIV#>+oiH@8g;eJrM3#P)F%|}B z5>^@!*B))h z?!O$jKE`N%qaRza41~B?ylxJ5KGvbsx9%TC$6?j}S1yhsTom*qlz~gB63q~>)HZ?0 zxUz_dWnzi+9s(rkH*!JhTdFS2aF7+-CIbg#X(Kd(ee<1xL4>bO)&`Z!IVQu zA4o!o$g+Bz@WB=dKd+707S&VRer}dNM;73`gJAB{qwueYif`cH`YwY+PV;vSHj%nD1-7o z!JVx(sA-x^2J{{q$74S~!rKgVj$@!xU5yr%g|TE^IS3&*_}a`E(6p-$7kz9A2nZIh zpM%C%viDQIAcErZFgF!bGc|tt*?$Aai*}d^Vp`$RlpaYc59Niyhgv9mY43zsO;dIU zJU)`f(boseiMD04=7*o0x>C&m4epLdHiMg9D+KTiz4S;xMIZ-K-E|R7w?D zIFN_xR+r&Dm)3gHzoXlzZ}_YEsH%^nsy>eDx>8it#ZgsPiiY#5jJ8cXdqD_6Bo;*V zQZqTnsD6D3xCTq}G6ND`>eox{l0T7-@x3x2F>fp%5dy^LMsVKM zi|~8=YtOGX+8)}~%We4LGtE^o02I$DKt)Ydo`^rm zEOuR;H0;sAt5n{5e#p#UmsVu(Zwl#R0h z!3REeh7v6iI9Ry69K-=vdjc0Ofa;}jeDYfxk#SwI6V3HEXc1uTd*`EaQ4G|jpzv(H z>rzBw0mZ_wNJgFw)u%_bFBZ#_k8&_zil!rHGcR&-LIl-IW6G{%dFM0VI_@}{5Am*F zxN461(gs_Z5`Tfi^o$ZLTsg-FRy^aNt_znEgkwR}EHCxSU6(J8yn1sX$jv`zG+Qb0 zok-H&Bl$v-b&r)y`8B`*I5%VDD5*3$4*@3tp7U`WIB<|b3geS{PM;uJ5JxBwR^M(R z4l*>2iA=(8MI>!-_@G6BI0}Nr$hawtP7E0RhXN6VgGD~@6p4vm1W5qlP?47^c10$@ zbt%Rs29R{kENRwBQ}uMJ$}l|>G)?;A#lJ%+5VqE(GD$RcJck#X?m;T!<##S#^%c~X zt+uw$xGA&@?82V*KVdxMMMNPdf(^CTVo})!1c*A7p=1}fAH5GPeLDdFi!08-<`tjk z^?%s@HB`jvv1Rp_v)=#U*a>Xg_Y1sv?3pRgdAgMU*HR2N=H{!vBP1{PLxBj^)?SJa zuDx|i{RW&6mXx21^Otz`7*ckySic+NQn_yABEsl>%Azk=%0ReA0tJH?|__?vUS zg_YIs%iWNgr&DzWIT$d-E~eNlk?ccT-(Ix!?ZfDVc%rqad>!iNZZNvZxM_5cG@&PP zMA<%AS$qcKg$qzsyaH9lE0k--CWkPY5!q4#f{k_8+V9a));V+-ZGC%jeBdo)r0wXJ zt-pz1qysEo@6h|78Zgr{K~zgq@S%OH$BZ8@6V%kT9Z&4N6U1>43&-(=&A;P&^Xtmb zL`&~(rF~+e4-dWmEjW&YMdfE<%Zi(MT~R?fDvIiq_D>%8Av%WM0s+Cr%WuMhxM#}d z7B8{B%VmS-@i%^i&Y@;d>f+ob*J5+SX8`~ug>x~tbTPUHo3eC3vv-1wrmm1c8lJ_ z^YO`Hr7jXG5rRfpJ9TPYAQgoW`%!JY$Zdisz+Qe+3;mZLB;fhW5B?=od)tozOoVISYBy5zln4Lox_I^a)en)Ws-1Rm)}Pwqo=jAIRUMlQV!lkN)17S zfglWtmbaTLnvg|^Eo7J=m75ZmWv)wuDZueIcGY8hE&(7G7EiFoCI^+Vv>9<49Ph+a zjsJ#JW>Rssg#~fGqNS7~TA)4y9-kO8d#C0-a*3rBv51M_z|esH(RSSZ(r1+g)kv}%9fJozsjFyE zvCmAl>WvpJQ2H93H2bI0eB^nvcBRourD|lnPXn16AnUksZOGvVvN&NVz?*7kwxJX# z2o`#;@LSZaAQ(kyv=SwSmAGKV4Je9;hv;3y&7iV6`!qH=h=W}(@b9%{Yf&Cs1lM)Z zH7p_$hXiris;^_&+)dcD_;0bQYBR5!NF^|u?9UPusD(w5a>S$aQCYea7p=TOnzD<| zp(g)avJ4u?vIbdkw2i?P)xBYS(ifE_^pS$QD+`s)c^dycLOlYXt zjE0)c=5W--8*NWmog{TB8c#fj^|e><@-u6%#8YqGiRPZ|cu(E6JkwfJwiY#IYmNR7 zbiRPJn>IDlg2#2|e#Z*Z!H$=mB^+Ab`3B(VXDUlr3#~>1#x`&;yk7N7M2XJEO0G7@>5Anh(1Os7Y+!V$p2GP>D8;3eyz(jf+lu{%n z`q1A0CI@9QJ%*8FKic~C3Wi9`85!?I=injiIPwU_d>>^1OvSl)pG*}_n`x4X+_~+; zN@?HFF*F_DB~rxlk6jR=q>oEu^YQh|9!7c5LcXf~r$;VC_s9{*Kfa`h56f7p)givcJ~W7W9z@<&k?TcG@62&Iz0@IfGuq9Q`MPch+heOQHM z^t}!|y#qkS78prp`PjlPQ$;FOCP<%4DD( z*$kBz)!?=(oe0ak@X5*7tDizFTsF-gD#}LMYn;+{Kw)-Rs4JMjeviQL z0lEp29Hb5`B#C!*zR5O^3av~{i#@o~Q z(2j5WCLw?PJXLZP2^1h$BN12KT})ld>*O*PwS)qNxMux#&``aF=ZVb2)xh`8YsyyR z6X*Q|Rq++7zsSl!mC=y2BNydifY$*V;DrOf$1k>hTBzB{5`_e^^rj~J?#khwtAu_s z@I0hc9;~}qyWnzE#O7ej$}eKsyz@lA>~J-y_xp+!%)#d_{4ENDqqua<%?8-2;hyga+_R8V=@b7>5I9Md1lj~-XaZ}onHVz(%NRsJNDoLuV#owaLbu9-qxkds??*Y;xGLgIM0e4uB90jhzraKytjo zCs44+hy5Z73q^C?FB6`QfkrfU??g%AB4jc>xaO>HgHm9A=~9$L$}pK8hD+0kgvu~B zIfky`Bj_G(#)i5p@zTLNaQ2daz;lgXu=);teF2FQx3=&8n!tCv;QkjZ@RY+(3MFb{NkYKF4F=#3@ zEe3eI#8?PIvA_Lk{OcoE;j5QFgitWTBNmnOE>ug$Vtyg@#E*J-%+G&hNe(~ZFXTAJ z)+x%1vUs!_*KPa(dWQF6XuK7#9=2yY*_FK``_VhH9{@p7WC6CU{tDXq-o$~9=M6t$ zGfU7IF%_D6Jeh^#2Ri---+$x^Os2j8Cm&&9qg0cga9j%CXsCM|y+jp#W z6=&ndi|$8z?|g7z0Rv zHg&#=f7yBof3n3LC)GiAm8iyudZ?Tj)Aw~s*SeHPJ>n2fx;QcqA3x_V>^ky&GWl1VNLxuy)$}l$)`}{tEu=1vcJIdb9=_ar{;C1aB-Umx zl35L|!mojB0(TO1#F*_$K@VnhASs4P7z?;NAyN^D$m3yLD% zHutf~9z5T8J3jTkyAY37<-lP2(m!nm2qxH%w%)xM812X*DXR3VF}}nIrCyY|E;Y1A zkL%ahUWI7L!NC)Mlt7(_71bX>{k+Ta&Tn4HS}dD?4Ju2{4`ssTg~KdBUc0wN`y{oXb$`vl%R_8X-v zk?g?3yFZGtiLShJYw6jB!Lg$l8a<+vA3E_6K6vIIaNo;uB$EA-u*^*I8O<=MBRd^e zJs?lA6nc)S&9H))xRIk&;lCg}f9EtN^2 zz3*vkT7JF749)ysAI*4sx)Ev;N0LlSdfAs^C5$BU!xq9fbn!40{!e`Qp{NKwMMe}- zf3;<6(L3@cP8Icz?DsYjYXQT_&`G@?kUQS@%9L229a5%KW&~r0vK9dR_Fj*yh0P3A z6xU&Ryv?kbB^n-Y#qz2v6@PWIbkZ+qa3)ajCt03EI~;Pc)Rmg@zl;&Sim)eOTqD>+ zA$aY5$*(L~iVtnL19cT=VIF{ezF!)J!?T;)fM>_NMSITac~DSzqNV*>au67qjD zMS=37g}CLit!V1{GoIQ1E0o2S;=<+EBN!-0A~}grpg8Njo(vTPi$Fnf)8)Uz{$uyx ztvA1i1!bFX<0b!ryPy3UUO9CCwELXJSCm*JphZ5smMH9_o{uG!AHr+RKSE2-lK_CG zuC16z58~tJ|6oQ8nDu|?Y>W9_<-5AtswYUJa}Wm>9XQTaLp6E;r=Pgjcw>1hsK)D&>o4&aG%m3+)rJVth3HAF1 zK`2;g&FxI2%pd!hZAxXvj7+MxS$G&>>79kC(kXjOlKOdZGxN!6iO6;`+Ye~W0GO`; zAi~ji5S`N3Q~{B?5RTV>7NErZvICZwq3r)5enuA4&3FBurc=%UMjj&|9;|SEkmDz& z8C2N;!G84|kRACugWfgFkTwL1m|x$D^3H+(ZD)WWmOp1!orj^6^0;3P zMH$KeGya`G1_L7DB2<+xJstNVZG2WiCh8xw_wB{NXadQU8jZB|9mL>h2ZqP(KkUtD zhKWl|q(*SK^HmIunf*6)?L=Aed;~+;Wx6Q4x00000NkvXXu0mjf5wD~L literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_read_only.png b/app/src/main/res/drawable-xhdpi/ic_read_only.png new file mode 100644 index 0000000000000000000000000000000000000000..43d966175793c9669525d4b9f8a927eeadb877b9 GIT binary patch literal 1895 zcmV-t2blPYP)4wld%Z%ck>5UW3$}e<-I>4^RwAZGwGbA z5k5+r95n;uGT_$pB5k7=2l#xcEI5jVJVgdS;^r-$(HDbDR>=GUUdoz>l|1jxFVX!t z_tHl^tQmlEzEf}uT*X)TYT*8STJ4TvH^Q-`Oy4pmOqX_X( z>|c86Nb_^p#5!q~3ueF^mmU@}i!_ZakORh-9u`xTwb!i=k5hA-qSxFw-RPu-pzE@2A9}q|H?~+ zc0a{F-sL_NDVkX+XY^&+Pc4n4X_9jDrWueH<^qFkF+)WI@G{;mdj@OrSjfhL6O&p| zddS=~y=I%#QxC*hFTMJL9AY8Wq_IVt8}cse$?yw(wurMJ0AicK1r}*sIjb+oepZuUItg>gT#--n8?B2vj6x8$+ZCZ$7_AFVJL~a@)4+q0 zl0K6*mqfFQGJMv^VSPp3VlB`j+e})@dCf2Z0O0Mkg(V>tR@7DCj-GcuK8v|e4x3x% zh8$ouWu#do-TI2yG~jdAoR@#-sN<;^0N`!6>sTJFj@AbM7@6#KhP(K<9591sNDi}> z7?otq2|2+dz&qx&8AErhxMRlN!8BR}EW}j3-_J-u*D5(^2F(pQ%}QXAdBu##LQeBX z<1;w`UdHRRYgm!Xf~vGy{Np(+K%9@sUNdYi$vxCy$#v5!{dDQ{*qVz30AP1;jZX&u zt(yFkfl42vMfRJt9N`f%Tr|hbRg)>C)hZr9a)jsn?V*NhPV9+*6FyC}f*#}z6yEq+ zOJD%%+B5U8RBa#}V@iCF;T#CdWS1F{tDMn$rxbwG&U%%?Q`1ykfQ3q{*6+`O0CP-_ zxnj;1qcv87`E|GR-uG0cst&MFp&I;C*>LoU%im18IK5W!-hU8mnLYlIP%Ko%9;Py< z)1FqWW3ke_&q6RN919gn{L^GmjIc$&KZO9CcGeRjRQbNA;;J^VmauH!<1$a{OT|Z4 ztaR_QES!&EVXk`W;5$<|9XlI&oMC*3oOG~UwKTLeKy`9>${3Jy93@49>y$-3>z`LM zRaL-^G9HR*+$O|;GfQ8~{%+&TL>5)$T45HFJeE8=X$G9R{6H20)iSvP$CX&uB3tToYEXpShN5IG4JlL68U z)8@={lh)@-%b1Am!GvK{y z$3sp0JU%f#3u6Olr#2_`X0c2Tnqe?nMp&8+$2eQ{+oK!5hw{)s*T8bS!@D+tEpTSD zG7$3_>Uhq1z$ph#W?`rJ(SOWCGQ}o-98$EFx7=wHiGj~>GcVLKe4WPtGYTNiPd+dY zko1$Ad7j|pG?S#;SRw!?!J^2V&u~BJG^bEO?{h$WiDyGg%Xn$*7d_DoG-jvSFblPg z059izpCjL6YY4u}@7<;nf>7G28AExRIVLHW*`xalbkH4n88M&L(HRkotbk82RzFjQ zCHwSn?mLdi4!VmOc^R=HsyaphkcVlGIQuMaagG=C?@X$wIikZZW8L^H<64Uzvem?KL6sVesS{O3|z%CIV^PZXwnpj0ePGT5_I!h zw(Db4@zKJ=?jpe|6tIu>WPbq$oH$S7@fUrSDHT`oG~dnlKEDFQoC-b^ai_cAb{Fv9 h6jyOa?#Tb9{2OSllaQM4uAl$_002ovPDHLkV1mYKcDn!o literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/navigation_accept.png b/app/src/main/res/drawable-xhdpi/navigation_accept.png new file mode 100644 index 0000000000000000000000000000000000000000..b8915716e06792d926f397a7b17346f656c86aa1 GIT binary patch literal 1546 zcmaJ>eM}Q~7(NOZ>jx?|Mx7)_1qW{Q?n+DPg;FVn(PM_rcwjwgk7z zaExq;FcCM$1T(WNW-4=WI(1VrnIo~NbIM>M*htY0#}vP^FxeFW{X=%i-TV8!d7tO~ zdUEwS*{>~}w{ji;01FLSdXr$+1>d=Igzo{O;<{i~@EK-4moDL57>fgGHrj%N1`;d9 zO*m%rRCM7f01z5Ogb|R~a#dE8&^Q=`4tJ)^jrh+MMR3IR69s}KEImMxFjdYS170y97Bn2l! zc!x&%v?#MN2TZ3~98}8S1S_0`fJq7&qJY&3^{b#9Mqmh$L$E3Vfl;Ljg%NP-kqX&Z zTM25?XHMl3ZW^hb=NS}&TrQW)l_;axQV3D2)etO)%@DX$DNsRmu?1h*=3L=$S+q zji`a*c#N{*2E9fqaL5S4h9>Hh6v?`@WTg&HMvx3dtw7Wn3SD|yrV>#pbjUPTPg|WN zPVv)N+cT_sR%}oVBqLvOnw4(~Yn#0m)vQ=ZC?~O*G{jdJEH8vU^=Avr+i|X?`*2LY9 z7jHvHE29PrBH!kINN(*uSt$Oev6h7iu1N7t^Bcq8%eH4kerw;l_*m?|A2AK`y6?34 zP`@{J#lYB+FlJ}1sp{bG{!_iCO_Q1c_7CzZkyMzhv`>@qo7TfY@Gmt3uSG*tgxNWQc=v|JYQSD*fn7li5 z#PB$#i7UEVf00h?$hp1uLQj2zQtZFGsWX7jD_#?wJrFj3Z4#6Vd{fyGdK_MQaQVc! gtQRZb>MwglC=~>3@2<3zJ17=r0mOtC4d1}7AMS~u4vrHSI9daBXx)X5fyM`V!Q zs*~;0m=F_VpltTttt?g3T4}blHd!#MY-b@<;Kl_32gQ?++i}Fn;clI5MHd&&;b&L| ztw8uDo$QgRT2mEdpjisiC=rDP$wi@DwGve$m>SywsSp%_Q5B466)1vhv^au7t4k(C zW33H%HBq`6OStJ|HlAm27`4OW*cD?!!Q_8!77zPKqxqmlPBE@CzrOSKv0~8 zwKKe(c0yrAvYu|{buz)}qZAy>x~!91Et60%*iAApszkymtpQD@|A#sp>u8RzrXKtK zPhrmNVJNto;^=19A~dccEgXu$4J<|SG;5~mBWqQxve7)v*=Pnb)M%6_WFjqgXZU4> zVKU)HC&!ac3uPp9GC@OWw_9-)YS0+4oIFi2LZD~~im6eoL|tqsD%GG`buqfeC1^{t zgL3j~Tlwjnn>3=T$!Oi--7g|e2iv;$g68ncuwVubxAtGH zUV9SeCwyEVT;Z}l`jk_ccUBneb9vYt0FiPdQDk;c{^A=c6BQ-ggwf*e~W(*o!KugSD5?ziMQt;##SV}PH?IDIgJf7uju1ig1^5w z{KAg29kY2UcfQ2HrWx>zSHyt0>k)b0!+i&p?eBi38k8K;8=B8g*p?qwOnIe*E7o@_ z`C!uZJ`$x;NrH?6mVZ& zD}uVkAAlRD>b7`m6Z=JyZt-2vmXVqbaO1_$V(+%zK#+SnkhpXy=H-~C`|NA2^4rPR zp3MciSo8ge0rDy1M|UjPhIe% zkEIK2Xq*VmEq~=7Z%mCa24_jE=Zsj)O|?e0<U3$ zL#KcyI3U-Ld+v$f9L^7Pc7`UU+u4vq5xg1^$WZMoc&YIA@>}MRuiX2?a^|j)v5@;T zKQ)%AA1c>$8;s{Y*>Lfa?r!#ypN;$T^KZiaT5n$3{^`gV&@=Z(()S+yX&@|PNhNW) HxUTIVM0g1d literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/show_sync_settings.png b/app/src/main/res/drawable-xhdpi/show_sync_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..64bcc5f7d9d19e0bf2ca04cd83d56d828d7f5c6e GIT binary patch literal 2038 zcmV`e|L8MNSxMg+)_L#xFCn1C?t;WY+^RWX?Z0<>W4~IC4~5f010RX zZBzkmh(b$1^g%_xAcR!GhYE-)9)UpA3K5E9<2fV_Eg<5i35dFh+dPOLYkNJH4|}E= zESs+F-OXCvBaOV8$G!JF{^xbCp++^TQH^R;ql%KXwy1lRAX90^MI`>c61WC%Kwd=7 z&-LE27eLI6wblc!2R;hCAGj8X60bU-7x+E!l8EFhAqZ0;%{KvG0B#0Svlvg3r-FD5 zcuGY6QEnlm%BlI`;bE+`Hv@ZB`?03yqxjiM;1=Ka&xy#tm#qa@Yi|Vh={#-&X> z99&icP>iedh{z&;V#uNHGMp`1G4 zv*ikvri|wQ6fjwY0{$Q(Kb+(HAEjH$(1Y~^Q9|7uo-~Sk}V;%_oad>!mtd#3bS3zwQX_9oeI^}2{=3S9~ z<*ExH56mp^8&||P6F{!&eSnBK;Gbm}djc3=E>Yl!hPYC$^OT6pRJ{PSdK&^>E@7gT z27XfYc|fA}{{Zl=MHWDFt?_3fGEvp3@GRy(St8~EMzlVdDo0-+T?Xbq1B{hOIpe@% zA~IF380?in^QBXg$n$N2_-=Ni}6>aU&;AoG6_DfcY@*=@_v9&Mw9bw{6>2pUdTb3H(QItv1Fy0Uwd5Q(pV2l|obuExq2J^G-Pie+BwotR5r!BTmAyBW(TlncLMLFu+28&dEQS4 z1_qwA*8Up!whl82JkiGUb!6wnw&(FaSO3F_g7jk7AZ(;_mwlq6^S_U(+0j^0G! zn9Mc`Cr3Qsl4gCaYTl6w;Kw3z&|146=+JefMC1-@?VakV4RFR7^CRcnC0*y_l6-(V zxuf&t2aBbNi3tz*ibk18HSd7SB687MyGt3GF~&R+hT$MUI-PzXpU>|H-VHQmGMSs4 zbI&>FKCQy@opWE+gy}wwE~WPYC93M;5SC9^Slsz-(|gm-xmPi%{%IyS~L@g~X4!EfOf#yU+X$XR#&NFMFfkt4;$@vA}VOD?Thvt~dY;WfZ3A~NXv zzSCr9R9k7cCA2zejM?M+z5zZ#;WWzkMC4F%{4;0HR3`*x`;ssWCxHRg>=!ec%nho^ z$JMz_a%F#T*z>&m!Z3W@Ik!{&#U;=4{<6^dt6_JTwQJYX*4Adm#>V1^a5;*i$Achv zBn-nt*4pQR)r#*+Q52mO5ofLal3GX}XmZXqQ8;5*nXYgNAwXAGSJoJ_dsfYH9q?P= zSHJ~*w!#>5y>sqvb)L!h>wvo!vX5Y@!2+zcn}9uu)Y_@WC%CfRKB3R+fcU7)wF-@~ z1f++IF?$qJ)f)z5TDNW;A~HzfXk6S@8*XiF-9h2_+}~6PjTBCh)}wm|Q51c|7_(PP z@dg#t(PEje3Jb_)vy6|AlS-vL;C^GwLFe3yA`)pws3D4?{lGikShO*ZhGE#3u%H%W z%nsmW7>0k|v}qHAgM-Tl0!2XN9-v1dkOOv$$g2RGH*e)*5_Glxm8M2Ds!@$<^j4t%0?4j0 U0zg+iiU0rr07*qoM6N<$f_f_4xBvhE literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/view_website.png b/app/src/main/res/drawable-xhdpi/view_website.png new file mode 100644 index 0000000000000000000000000000000000000000..aba86f0b891a0ee2057ddfe3a70d8923d45ccd14 GIT binary patch literal 2638 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/addressbook.png b/app/src/main/res/drawable-xxhdpi/addressbook.png new file mode 100644 index 0000000000000000000000000000000000000000..c23e339e8b07b243fca2344bac0460495714ebd6 GIT binary patch literal 2817 zcmV+c3;y(pP)pT z;1Td_wMKrS3nQ`wgup29J`fsXioh%0zgm!#If9%bt+hze!@y^N0safz1g--W;JJ`g zZO2GwSAMktNPrwdiOesgu;)yo^)&alP zV*}6-c6@0lN0eod>%bY{ync5#a9lGcUkNVQvW)D00(0(R=LNy@FYIM#xCf@{DR zGQg~qe^{mK5U^g+GzRQ6#`HVI%jmfQJSnTn*(on~kN9z}2Yv*u0DlI)(tCS>Pl2QQ ztY^SW{rofFcffYnOLSw`nMvhSsZ%62q8;0R@rBbQuy9vXPN~N;44B($|7=~McgTMiK_-qr;FDd!{Q_8;~ zgzQlw9+UE4#Ej=k*nagC08hd&d|s(kYL!aGIYFjI$QnU!2|wi~N!MZL2Vv)LQhp$h z{}lL02AXYxE!RI(yBSBtPNK(!yfLN_hT$;qb9KIfhmJUQE&->lwRb$vZ-$*#($zRB zb`m}A1j8^K5Ozj|hnF5Q;@COudH!P9X(e5YqpT1#C?sr);iCY&=2bgBcJ?N~&O-f8 zSK_GHDf~FkGR6!_nMZ)10CQ4gzYH)5Tm;Up06RT{qhhD<<5fQIXoRp!o?O*&6f|%< z51g^q-qv_wx!CCm9Cd6~;*A`3D*_6h@&|x>fCav=)=uj84+0-*NVin%EQX_khocR^ z31iIH*4o;tL69HLVHlQxw{^nr$csx-$OC!)xV83yrkV$UV~T)5d2VeHw3NJPs(Y_@ zEslZ?M!-hkH^R@G^5#IL=D4tPSJ*iOd@LjMsUm75IF}%%;KlyN4fqPG#3q1$0C%mm zv+?e|XR9EM97a_04=eHZ6D_!p11CM@A0}Fo9A$nC-AJIsBGD4&eLa`-dZU}GqhdSp zwy z{i@%GJ!PE}^PT4(6n6GY5ziBNZ^85YtBR!CVHiGE#k5gEHEuYAvS)_N!q&h0L13j_I=a6dn z5#WZDbzY@r8rV4tSZnRQN~O|J^fkgT9FpO8co<3&2_3~zF>U3$S-;Tp2JkC2T~okG z;GF0AE5XiVVdwAG+WWEN7!vaKs@twFv47|ojtXtW&+9}x+uoP5P5@t7Yo~=KzuHNP zow6|Iu=BZo_P=)r(c17YiS`u5JwHk5-LdMZm?n1oS~(2Ey&5;yfeT8wPqaRJAZ_e; z%0H~{9r6^}(7E>M5qO`SDUO0Rcz&SDsYNlTEO5 zoM=nqHbqn|cAix=fMtz8YFRgH?Lg1~^IJo%2s@8Dgq;t8E&ANLcaHwb+6ff{1+Rfy z4vw~^funYnp;h2(;DNPv${16Y(vN!O=*$waGcN2rY@+K+*3In@F? zM_ORV?IUt4Yv-)B*Y$U9>&hES)CH2QFU;wAgPf@Vd=Grb|F3ChhM+lM(o=pnVCSzy zJF6iIqgh3$+wxvqVs%tkm%Uws#ZsmSitW#D099kmI(d9n*qJ6;(Ek#+=XbKkm`&1|rY6`~C*@odcJ9T#XFbuv{ut4YJXhTW332VO z+?}-6uJ}TcQcBoyue}DmC*^k=c7~LP#>DPm2_p)JoqOtQCY{u!P+m&Fa9Z9&Cuv7DfZJA&v6$A7zc4|7dAS>*oG7Hi{6g60Lvz>AW zXFKk@t_WMruya`0*|iMplw{m-VJ$W6q=lfeM$I26G6|=@f!$I^j^0$bvEEF44-;A)>7#ox+Z50vkm8NIQ<5EDTWhny=kN zD^V4qt%@P@*NAljJ4Gq~iKqOIVkZj&xHZ?8QtoEpb-g~@p=!q|zYKgU<)7+QwUf00 z;*n7FN>jIQ$L-gdCb}Yiybai?60Krg04`;Poh%Iy(ELs)ZY2GTXaW7KTR=|{J5}bd zN4RLMoyrP3SsI{~g8GPd7u*2;Vy&&DjGcaY{uzxwp3)^!>VR@xDVnredeLM&u@dam zr2GkC=c$jKtnVFEYNO(V7JT*YHr)Z_vIV=x115kquSkLqf{PMQX2c4H-X{qHv_A*-tUx2p6kwHV;J zN+@LhO3F9YA+IXzv@-&V-qiDgy4b3ezh8*LkMn3q!gV{KD(cRqip zQgcqpwY~_}l3vr*bJ$7C0D)(KM$9nlq-6gVWP{!uCdow+NspoDu#+}|3b8VCgKGAw zG3K%&Vhhp2{Wck=N;)^)=t=CfGYPx9pS&d`45@A(A-X}w;1bZXYO#}c8QO>?&;rrk z`TeS7W)u}0#s6ALp5L?BX-5P!VqNc?3^B<3y`UG{Ik9H2lcoUzFAoF@yq9!=e6f5} zmT`L;J84!yL6bT@K4Pe~cz-ztsH(~9Y3!tBfClqd4mw3Q+Hj1{&~@Mr(LJNj_cC_U zGC-YZVmYaPpqj92U20w_c>lfw-0F4gumCm^i!o+^=#~H@UP;)_2$*vQ0A`kZUG8d% zfLWr2?INAI_nfP)=GtWF-jQzj4-i)Ce{GmcF1h5AOD?(Ol1nbR?zjI+`LBWN;4YNeH7&|RHraHZB*2x>{neV^(Ro(Y- zNb**HUtPW3DwjvVi9@jkK}=Y%rfBbnyLsV^74Pf*Ca$#&Y^XSRpZP`HpP#e0e$(83 zKFNru?xWWS#s$Ue0~-#MD90~maQa+V`c!vnH9u=WLH)GI1f!z=f(_?o;&*%xVmSC< zJ>wN-zLj?kMI1WB4oqlR2Vx!gE6wm*u>IZLjUcwv@t}sgdE55}mfc%a^n3wB7oVRM z_dB7odS-`1dtYzT39r3Be+J{fgZd0IEdG@qrSmneYOausDEl>KDKG1X;(F6fZ@y@H zYjZt#{!ixo-2Dvi9KAiJhvo5been8_eD<=h*Ke16A*1H=dCyNq{r=iE;k)^!6$Qz* z2F(BTtzUOIUt2k4=}I5Rw@$}={29WfvffvVU%kC;U2cX`H)cQ^C5$j3_^ zeZXIPMxw|uPT!C@>%_b7J+4hrd2 z7I0Xw+jq0h-89{X=rctn|L^N?>{V#5Q+fH@UvyIHq8Ak$e|{8+Uiiz};6F3O?)naf zA31AnpPR9o8gu+Bj+A=y>F0;2i_*n+D=SwTBF8hUvl&3qzw`NgGcP;ZiisdZ004lPy`7aaKYe~m z2+a58N3jO{0Q+KWtpGoMlJc^O#BYT0cJ5IC095j)fBx z9Yv%O7Vcsrs&Vft9kenZPyOj7`z;6M9Aqaq;;z(?JmMKVTwH zwk=0jpBNRKk-CKdK(L%Xr+Doqk!V7lm6Birma|lMaJr1NGy?DjjDRfTQ8ej~BHJYA zU`$A<<1{Z!ZOxJ?Ls40^cJ_jHfRaG#d%gr$rUTOzo?I<04|c?;cnWUP13yp%LjjFI zSlXc3X)x~1PSLoqwr0~CeE=k&cA-zsoyy-b3KTi;rmM5YH^U( zGvPkynS!Mu@NUsCj^2Z=q%%)!{XQ=iPC}NcHwAj_JOghMvT1*!p!?3vU8TCJ(lnh& z`H-*h^jX$L@BoGK;cd6n^zrctsi9a|v~lCxSAc-dsk~!_*b7wc(jx4ohnrIHuzcz} z(`W7rs>a30%xhtrjw_fIwUc^B-?ZNVO9$#pFF{el8SmVFv1$G^g`fsXm`>)Wz1PsT zP#Ty<7AxFHV5|mVW~*wbwP836V~j)FrD+7YOlP18JG3GihP#l{(5BOnTXw;&f3aI2 zNu>M8I{n8m@_xT?sR44r7HFN`N=$?k+j46D*cL^u&DE4wX&xDuD78s#o{OGC zN*ZqzeA82x$PG5|c@FAQ3rTiAwLKCMD9;ASl@A2Vh`*z3N?qZqT+ z6KqGj=c!A{f2da3f>&cNoLOE&=FZH^MW@=&$S8(f%*KuJ%TQuaYL)U7+8W^ z`iGqy=9usl_79P)m*)XI-i8Y8#BH$%&+PZbk-1+KhhG%Zs zN?B`nEco21o}~rJ?7!`h`@U}61voAG{b&LcCq7oU`clU%;Fimm)aQk}biln@{md(s zW%tsA5G$?r6_QrxU`V~x+9b2y83+656{kS9@ML8I262np%+bRWaf_g!} z`@4{%3tS`eQnPdZIukED#t`BStNkxAkf6b|<$@l(#!|1hSQ(j11S#`kY(??K)K2>V zdp0l;UEQX1z+i0E0xu$db2(NWfaSQTy|74WT3p(u^qcf)r*8#x!c{8{C$L-U6*iv- z$I1^Z5N08zPFiOZ?if)wPNm12#za0UJ=cUT`@E-%Pn~JqC8B7;5xRz4ULuQ~Ka}AV z(js?LU{Sr|G`CwRyhHw~$FLgvlPO_~0AcYCe-B15oA>TOX=^NYMl&)NGc4HoF<3V9 zt#NMUuZ7ml`7e&cf8FYkFY*|sG%;+I&D~t_frg%>I*u5Ol~o&IE#*%{6_!CGhbR1V zKQ9Zybi~>}$o{qAKe+x~eG?bdNm;=gx^n-yo#eJ`s`^E^tz;|}+xGNJOO!b&Qj5xx z%-4^gktZUkZRIlgp7~o|g<(PKAefHYF3Pb75{fye+ZvTk%MmrGkrTDm)}J6&5F60VMH;B>FA^gQ|frQcTqM?o1W1 z?cwaY3-Ruq@`|ahJ${-isw6pF{SmUAktPD>qTcu*;qmv_{)b$2H3rM!^dOjj?8GqA zU_`0=P;O?o_ijA6Bzoc9=yFVp2(Nquj2{xzy)ZJUqKU)CH;OW|ppnY_HF!jtYx3tw zz-YcE@~4L%7IZkvufHi7WCcfRsc38!0q@S*lTR-y-IZf&lmnkCZ#F)w24^7GCcVd3 zeMBXK9w&c5uOtyVH~bycbcY?TI^5~hK3no?OfuoN!7{~i+6*@jZSv^7<*d)6V>K<4kan?0?`iDbP? zeuQ84u6QlB+1PaTHJiC@$Irfp3>k@zssze6YOu-PlPZt)1+@VWgjM)Yh~ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/extra_actions_about.png b/app/src/main/res/drawable-xxhdpi/extra_actions_about.png new file mode 100644 index 0000000000000000000000000000000000000000..c01120fd392f9ffbce1611255b42d2bf7c6e5259 GIT binary patch literal 1436 zcmV;N1!MY&P)pFeXt-> z762An>s$yZr}#dA0f4^apPmDlX|3l%0B`LB07D8fK9K>K5*hiECm>0Z0e}(3^(v7A zm}sqMPm+KnNxA?onPj?6VM~hX0B}dv4Dy;&0LS!uXL}Hu0vKzp(gzNjedoLcJfU$oYL*HwubW0p#(e*nB>NH_xUQYm$7jQQki49`5vXe%>Q z0PpH5{Wd{kct>L@;-OS-o(E8=`Ks55WO9x#J%%1Gsl9ihe5zuG8{sXb@-;h@gd|DcTPn6o3RPi;AgV0B zWnO)7k#I@*R{*}IC5RZn3jhO3n5zTB(Y;dYo+(jJlv0^7W*KYk)tzm5mDkziF-i z^Dsa73Xk`rkBl+5!IYttnEDoL^)-9_wW4Tr!^~;=p@kss#6sG{*l)=NsU_qFGZEtm z@Up_WvAM!pt0&TDc)X_7vy`f}5Wr2-7oLnVhJ8EpU&~U$p;(lZIhYWXhp^J=IOpUg zyuNw@C?piIMEkHTDSPD}zzrg8ApvPg*()c2TN_!mkbqT5*()c2OGbAsB;d|RBKEZ< znYEArFETxYpjP-IAa0?qx}g%lRR^anBp~up?YSU;TN*xYApyrFWv`rotSWa6Fa%|_ z6JS~VI%^>TXC-B?B?%W7t0G_LXzBpT677(=Rsy(W)N7IT_k1K^pB?}>K0Qz~1|?+< zih!<*)xrS6Ps}A?SM9D&2%zJJ+>SCO5M9|eA~0y z`2ml6gF%zZpMAH~%4HoqGOSx_n}1nH$vFE^rpk3S6_6koHq}MIhC=qevTU($IFKvL z$|>moT-ujneNa{|?Q>X($hDOLOoZg()u8m8M`|k%~fFGSJfD(Bb|a8yy+gz({FJH@J|U6uMAX zDygXhMh0?`+$_IpJHF4k_d#t^+sXT!?>nE*1tEkGLI@#*5JHIBXf^RepPij`f$q}F zGay;~;+&I!ibEd)1E3FdHvDMPEca4?Aohj3W+%A#m)+|B06{04{fE;Sx|00}G>?#!f{9B#T+V%z%LVXazXu z5(5GrWAB2dF(2Q8fJwk(rv$!V9J_#V^rIj@Sr$S<2K&bQwFxK@ ze#U%h$uaX)(z^~yj&_XUTcWM-o7n~J68;$Ywe;G(BCogzZy;5{-D3?95+DJ> zu7HfNOAezJ5cS+nU8A784ew19{+0YU;KKuCZD2nmn?Apwo! uND*+u{F&ngGeshV5JCtcgb+fA1LYrz!UJr1@D&UI0000sy~D}K$kM>TG$BsB#Ps5+b8G_5&Rb5m2Xehw z(EGO8x5tSqh}qzn$QI8Jv(ld%)-eX&>AJE=zW1QF7t<02CJqM1T?@Qxjz?UVsqXvu z<@xLH64eji1UQr!?EkoF^-Gm!Q}f@h-W{L1z3}1lU(eL*)}N2L&Ul(--TFU^J)#<< z92%rP#IH_}{*=bRxFeLyE5-bc#L9yCOB+0;b|>y`JGw-Kg~@GhQ_zfG$LCyUeAhFr z31}h6wFS+k2Oe0~&of=Wu>Ai|W!9Bo8y&zV2skisoM7L+-2AiPV{ZutFlZ1g5K3T367}x3y}0xl ONXXOG&t;ucLK6U0_1k~| literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..126ae37e05ea4a5aaaaf89b1b0de891cab92fbc1 GIT binary patch literal 12150 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 T&KWc200000NkvXXu0mjfH;u0r literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/navigation_accept.png b/app/src/main/res/drawable-xxhdpi/navigation_accept.png new file mode 100644 index 0000000000000000000000000000000000000000..6fda89ec7d8d2ef1caa50a1191c1b2f9a0e65e2d GIT binary patch literal 649 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGok|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n6y1z978H@y_vn&?@)k*!|Ik+M|N2r&hGUM-TIHt7{92t zFt#xG%y-U5#fNXrtdgR_gu;*6Ol=dkEd74(-v0M@Q3OilarG)?VsosHc6X{kFE8|r_utCc zb^hr(TDA9H+1%eud(S;t@AFLE!QrP&)2yfd=O*mmGSR;w?dN&j*=t???S5Na$gyg= zeb&VN1s`izI5g%nG(Htp*igg7GHtz`L&ncl%tsXBa~mR_tmYAzTvN)JG(^Jy*f2`2a7taYt1E~`Fiv++F%=bn3Gp8Vp#eT5CeKiL;`Oqf)& zn8EbZ;*JTk{%}p+uaf_5ao?*sCnY~sSFxVB_gA_4XZxMn35IKKo_VdU82aa~Rq{bZG@b+P#Mt@iqco6CE4p82`js^K+H4XA>%&#t@~3O;?3l3o zPaN|Nw+jmUzcVZo+;U?5hi}uL?37nb)!BODyVL^bme|veZ=U#G#9?G1!wVFWTj0zh z`l*@4pl8C;KM9OhZYRR(E-<9KortRYz@RI*X}Y{?!z>Q1C+@-zR1Qt;*~#`3B_4sC cpP$qZFdVVp7yd2n{x6WEr>mdKI;Vst0Qu`3kN^Mx literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/navigation_forward.png b/app/src/main/res/drawable-xxhdpi/navigation_forward.png new file mode 100644 index 0000000000000000000000000000000000000000..a4e6f382532da28c765c9a5b6394cb80e501574d GIT binary patch literal 578 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/view_website.png b/app/src/main/res/drawable-xxhdpi/view_website.png new file mode 100644 index 0000000000000000000000000000000000000000..05de707e2afbec67b7395e20a0490d356d43633f GIT binary patch literal 2703 zcmV;A3UKv_P)Zh7CLyuefjH(P(yeW_M;KTl+OSPP;oh zzwiGy-^>9$_0&^OJ@wR6Pd)Y2Q%^nhbdzY~ZG36Yc?=-dzRrXYSg>f zV9&dv48U9naekv1KoUQ)1U@6v5d+vFUm%1K(-NNnGGX`Vuk9GKtI&vzj)9K>OdOnP z4&VsDHh>)ktV1D0?l9&901qupTnb>?rH|O?5d0l7H<5*XPUyNNML@A8tw&8jA`LmP zVESALv3MsKz&VcqjBBK?kmd-$2?0M;q`r(!g%ERxcNtkcQx$|0TTN?9n}NaK0Yt%;1s~z!dWMQq+e%pFecpU5c~|l_dp8&W+ zs{U_^bZ!eFW>U&jO8J5@28v+HMU=5KDdlSmKQw7Et=j-TVvIePQog#j8c+lNp%9`l zDMb#z04zxIIe?WC%Yz*3NtN@ibCLx9zGHHBN+vP5CfxD}egt5T5+L2|d@qE^IOj2A zYy#jT`u-&W{5dh?Ie=Zp7-x*Vlu~9=$`xbmF982A4ErA`Wm+Q;dk)TeCWQExlrm+E z{Q}@a0Pky>aGx>uOP$tTg$9fb6U|9-CYV@9(m}Kdp=87h87r>teQ=v-kG2Q=}fq4a(dP!kbmW)x2 zY3yirE}1$ZM9w)s2JnS8{(y77<0Dzy^Z7k0hOpZuV{BbifG6HGOtAyws9$WzxE! ziDqOPo@lLNRh>GEYOryyS#IreVM< zF!IWPs4WAk+W$y>cN^4o>VTiBc?y}5i2~LM)}3?v1}2k-0qM_*{(BJKK211hJy@O4(GN#08g*ST&f^72Gbd=HyxrirFCV{bkpxE-508J?iSGyl#LpaGO%se1RTo$#Z zHCTDkfDk?WBZS`@AjmAtN`&AH>-7ZY5rY_)*A2WD<9wKefrr?>)&bYmb}G6b3I{8& zUIRE1LOk{Tf^B@j9f}6(8lw`d8>sYk*Kr4Yo;s964wW+ds;iBCZ5Y7FaeZ5nH9juX zmUCW6)VyU$!IqqNN7h9>ob$vtV_Zy-$)?4$l4NqOj}11Hp~dQvYxo)9nT*aodn=L z*FhY!n<&GS*rHoD(s6;Z9B!QQ8J}}vAD4EL^as63?@n%@B*A@qP_;RjESvyLN`PtFTV%GGmp*H2{Ioxad zY17Dr?mI6uh-&v^4`JuPMJr8v$k3vNzHu>*OoOWy-0|oimRNT(wAKR0>xTyEKku9Y zM6TH_b8X$Lp}PS>$tvmJGT8oB8UCgRDRhl}cRey-+ru*=8=)xa;7Hq^Gv{MHVCv`; z6OURj=a6d0?DOB@ccB}ld%LiHI&SHl2`(_Q=lYr4Ui?(@CwoM>Yd zJ*#>X@6>0N)$5UEQp#71vFBJH$aLE#0+CV*2jC;d*yjk-+VVkh{24KLPn$z=f{XMy zsP{AD9GQ^WGC(=pBSAQN!5I4(>jOd%8hqbAKs84a>r=6OwXhf9hi>HEGbCEUzz@a% zjR~uk;1>x{*Gr9s!wm+0PzESWIIaj$w=BZZ5w51uYl-zvDK^iuFNHOT#^* zj)8x-4(~Q(uT{s_XIL-3EU~Vfe%Uej@9y#ShU~J7-7&UwGk(CKB%$hm!sM!{{}G>S zLjBKJT{G%`$nE+v=AHQ|!=8HTsi&TL>Zzxmdg`gCo_e}j^nV*KUpz31;^F`R002ov JPDHLkV1l>j?LGhi literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/account_details.xml b/app/src/main/res/layout/account_details.xml new file mode 100644 index 00000000..40790aba --- /dev/null +++ b/app/src/main/res/layout/account_details.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..1337c4e4 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/add_account.xml b/app/src/main/res/layout/add_account.xml new file mode 100644 index 00000000..5bf370dd --- /dev/null +++ b/app/src/main/res/layout/add_account.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/address_books_heading.xml b/app/src/main/res/layout/address_books_heading.xml new file mode 100644 index 00000000..f97ecb90 --- /dev/null +++ b/app/src/main/res/layout/address_books_heading.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/calendars_heading.xml b/app/src/main/res/layout/calendars_heading.xml new file mode 100644 index 00000000..770eb099 --- /dev/null +++ b/app/src/main/res/layout/calendars_heading.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/login_email.xml b/app/src/main/res/layout/login_email.xml new file mode 100644 index 00000000..8e5a0a78 --- /dev/null +++ b/app/src/main/res/layout/login_email.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/login_type.xml b/app/src/main/res/layout/login_type.xml new file mode 100644 index 00000000..8ce0277e --- /dev/null +++ b/app/src/main/res/layout/login_type.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/login_url.xml b/app/src/main/res/layout/login_url.xml new file mode 100644 index 00000000..a712b08f --- /dev/null +++ b/app/src/main/res/layout/login_url.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/query_server.xml b/app/src/main/res/layout/query_server.xml new file mode 100644 index 00000000..5e3f83eb --- /dev/null +++ b/app/src/main/res/layout/query_server.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/app/src/main/res/layout/select_collections_header.xml b/app/src/main/res/layout/select_collections_header.xml new file mode 100644 index 00000000..521dc124 --- /dev/null +++ b/app/src/main/res/layout/select_collections_header.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/menu/account_details.xml b/app/src/main/res/menu/account_details.xml new file mode 100644 index 00000000..e3bfc0b9 --- /dev/null +++ b/app/src/main/res/menu/account_details.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/app/src/main/res/menu/add_account.xml b/app/src/main/res/menu/add_account.xml new file mode 100644 index 00000000..6892f08d --- /dev/null +++ b/app/src/main/res/menu/add_account.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/app/src/main/res/menu/debug_settings.xml b/app/src/main/res/menu/debug_settings.xml new file mode 100644 index 00000000..40f8ba75 --- /dev/null +++ b/app/src/main/res/menu/debug_settings.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/menu/main_activity.xml b/app/src/main/res/menu/main_activity.xml new file mode 100644 index 00000000..132a9981 --- /dev/null +++ b/app/src/main/res/menu/main_activity.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/menu/only_next.xml b/app/src/main/res/menu/only_next.xml new file mode 100644 index 00000000..bd075ce5 --- /dev/null +++ b/app/src/main/res/menu/only_next.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml new file mode 100644 index 00000000..64132a6c --- /dev/null +++ b/app/src/main/res/values-ca/strings.xml @@ -0,0 +1,142 @@ + + + + + DAVdroid + + Lloc web de DAVdroid + Següent + Ajuda + + Error HTTP: %s + Falten capacitats: %s + Error E/S: %s + URI invàlida: %s + + + Gestioneu els comptes a sincronitzar + Gràcies per comprar DAVDroid a travès de Google Play i així donar suport al projecte. De totes formes, hi ha dos incidències amb el Google Play:

+ +

1. Les comptes poden desaparèixer desprès de reiniciar

+

Podeu trobar-vos amb el problema de que totes les vostres comptes de DAVdroid (contactes i events inclosos) han desaparegut + desprès de reiniciar el dispositiu. La raó és un error de Android + que causa que totes les comptes de les aplicacións de pagament s\'eliminen al iniciar perquè els fitxers (encriptats) APK es + carreguen desprès de revisar si existeixen comptes orfanes.

+

Usuaris afectats:
+ * Tots els usuaris de Android 4.1 que han obtingut el DAVdroid del Play Store;
+ * El usuaris de Android 4.2 que han obtingut el DAVdroid del Play Store només en alguns dispositius (per exemple, la majoria de dispositius Samsung)

+ +

2. Els comptes poden desaparèixer desprès d\'actualitar el DAVdroid

+

Podeu trobar-vos amb el problema de que totes les vostres comptes de DAVdroid (contactes i events inclosos) han desaparegut quan el Play Store + actualitza el DAVdroid. La raó és unaltre error de Android + que causa que les comptes del les aplicacións de pagament s\'esborrin al actualitzar per alguna raó similar.

+

Usuaris afectats: alguns de Android 4.4.2 que han obtingut el DAVdroid desde el Play Store (es coneix per dispositius Nexus i Moto G)

+ +

Si esteu afectats per algun d\'aquests errors, sisuplau instaleu el + DAVdroid JB Workaround.

+ ]]>
+ Benvingut a DAVdroid/%s! + +

DAVdroid es un adaptador de Android 4+ per la sincronització de CalDAV/CardDAV. Per a utilitzar-lo afegieu una compte de DAVdroid + pel vostre servidor de CalDAV/CardDAV i els vostres contactes/esdeveiments es sincronitzaràn de forma bidireccional.

+ +

Per mes informació, podeu visitar la pàgina web de DAVdroid. + També hi ha una guia de configuració. El DAVdroid respecta + la teva privacitat, feu una ullada a la nostra politica de privacitat.

+ +

Si feu servir CyanogenMod, "Privacy Guard" ha d\'estar deshabilitat pel DAVdroid. Si no es així, el DAVdroid no pot accedir ni sincronitzar + els vostres contactes i events.

+ +

En cas de problemes, sisplau llegiu el FAQ primer. + Si trobeu una errada que està clarament relacionada amb el DAVdroid, afegiu-la directament a + Github en comptes de contactar-nos directament o de donar una + valoració pobra de l\'aplicació.

+ +

Codi Obert

+

DAVdroid està disenyat per a ser un proejcte de codi obert desde bon principi. Sempres es possible compilar l\'aplicació per la vostra part + i utilitzar-la gratuitament sense cap obligació. El codi font es troba + app yourself and use it for free without any obligations. The source code is + disponible a Github, i també podeu + descarregar l\'aplicació al F-droid.

+ +

De totes formes, es va fer molta feina per crear aquesta applicació, així que vam decidir ficar-la a les tendes comercials per un petit import. + Si voleu suportar aquest project, siusplau considereu fer una donació a DAVdroid + o comprar-lo.

+ +

Llicència

+

Copyright (c) 2013 – 2014 Ricki Hirner (bitfire web engineering). Tots els drets reservats. + Aquest programa i tots els materials que l\'acompanyen estan disponibles sota els termes de la GNU Public License v3.0 que acompanya + aquesta distribució i està disponible a http://www.gnu.org/licenses/gpl.html. Respecte al Google Play, Samsung + Store, AndroidPit App Center o Amazon Appstore que requereixen altres condicions, els termes respectius apliquen per les versions + que s\'han descàrregat a travès d\'aquests serveis.

+ +

La traducció alemana es realitzada pels mateixos autors. Les traduccions a altres idiomes han estat contribucións de varies persones que es mencionen en el seu idioma específic. +

+

Traducció: + Català: Sergi Almacellas Abellana +

+ +

Llibraries de tercers utilitzades

+

+ * iCal4j (New BSD License)
+ * ez-vcard (New BSD License)
+ * Simple XML Serialization (Apache License, Version 2.0)
+ * Project Lombok (MIT License)

+ * dnsjavaBSD License

+ ]]>
+ + + Entra amb una adreça de correu electrònic + Els detalls del servei es detectaran automàticament amb el nom del servidor. Per exemple: elmeucompte@icloud.com + Entra amb una URL i un nom d\'usuari + Els detalls del servei es detectaran de forma automàtica amb al URL inicial i el nom d\'usuari. Normalment utilitzat per servers allotjats en servidors propis. + + Si us plau, introduïu la vostra adreça de correu electrònic. El seu nom de domini s\'utiltizara per auto-detectar la configuració dels serveis. + Correu electrònic: + + http:// + https:// + + + "Si no feu servir cap encriptació (HTTPS), altres usuaris poden interceptar fàcilment les vostres credencials, contactes i events." + Nom d\'usuari: + URL Arrel (les coleccions es detecten automàticament): + Autentificació preferent (recomanat però incompatible amb l\'autentificació Digest) + Contrasenya: + DAVdroid: Seleccioneu col·leccions + Ni CalDAV ni CardDAV estan disponibles a aquesta ubicació. + + Afegir compte + Contactant servidor. Espereu sisuplau. + Quines col·leccions s\'han de sincronitzar? + Llibretes de contactes + Llibreta de contactes + Calendaris + Calendari + Seleccioneu com a molt una llibreta de contactes (Polseu de nou per deseleccionar): + Seleccioneu els vostres calendaris: + + Detalls del compte + Nom del compte: + El meu compte CalDAV/CardDAV + Correu electrònic: + "ORGANITZADOR dels teus events. Es requereix si feu servir la informació dels assitents" + "Podeu fer servir la vostra addreça de correu electrònic com a nom de la compta ja que el Android utiltizar el nom de la compta com a camp ORGANITZADOR pels events que vosaltres creeu. No podeu tenir dos comptes amb el mateix nom. + només-lectura + + + Configuració general + Configuració deputarció + Desactiva compressió HTTP + La compressió HTTP està desactivada (mode depuració) + La compressió HTTP s\'utilitza quan es possible. + Registra el tràfic de xarxa + Tot el tràfic de xarxa es reigstra (mode depuració) + No es registra el tràfic de xarxa + Informa d\'un error + + + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml new file mode 100644 index 00000000..c74d42dc --- /dev/null +++ b/app/src/main/res/values-cs/strings.xml @@ -0,0 +1,141 @@ + + + + + DAVdroid + + Webová stránka DAVdroid + Další + Pomoc + + HTTP chyba: %s + Chybějící možnosti: %s + I/O chyba: %s + Neplatné URI: %s + + + Spravovat synchronizované účty + + Thank you for buying DAVdroid via Google Play and thus supporting the project. Unfortunately, there are two issues with Google Play:

+ +

1. Accounts may be gone after a reboot

+

You may encounter the problem that all your DAVdroid accounts (including contacts and events) are gone + after rebooting your device. The reason is a bug in Android + that causes accounts of paid apps to be removed on start-up because the (encrypted) APK files are + loaded after checking for orphaned accounts.

+

Affected users:
+ * all Android 4.1 users who have got DAVdroid from Play Store;
+ * Android 4.2 users who have got DAVdroid from Play Store only with certain devices (for instance, most Samsung devices)

+ +

2. Accounts may be gone after upgrading DAVdroid

+

You may encounter the problem that all your DAVdroid accounts (including contacts and events) when Play Store + updates DAVdroid. The reason is another bug in Android + that causes accounts of paid apps to be removed when upgrading for a similar reason.

+

Affected users: some Android 4.4.2 users who have got DAVdroid from Play Store (known for Nexus devices and Moto G)

+ +

If you\'re affected by one of these bugs, please install the + DAVdroid JB Workaround.

+ ]]>
+ Vítejte do aplikace DAVdroid/%s! + +

DAVdroid je aplikace pro Android 4+ na synchronizaci CalDAV/CardDAV účtů. Stačí přidat účet DAVdroid + pro CalDAV/CardDAV server a vaše kontakty/události budou synchronizovány oběma směry.

+ +

Pro více informací se podívejte na domovskou stránku aplikace DAVdroid. + Najdete tam i návod pro nastavení. DAVdroid respektuje + vaše soukromí, přečtěte si naši Privacy Policy.

+ +

If you use CyanogenMod, "Privacy Guard" must be disabled for DAVdroid. Otherwise, DAVdroid is not allowed to access + and synchronize your contacts and events.

+ +

In case of problems, please read the FAQ first. + If you encounter a bug that is clearly related to DAVdroid, enter it on + Github issues instead of contacting us directly or giving a poor + rating for the app.

+ +

Open-source

+

DAVdroid is designed to be an open-source project from the very first beginning. It is always possible to compile the + app yourself and use it for free without any obligations. The source code is + available on Github, and you can + download the app on F-droid.

+ +

However, it was much work to create this app, so we have decided to put it into the commercial stores for a small fee. + If you want to support this project, please consider donating to DAVdroid + or purchasing it.

+ +

License

+

Copyright (c) 2013 – 2014 Ricki Hirner, Bernhard Stockmann (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. As far as Google Play, Samsung + Store, AndroidPit App Center or Amazon Appstore require other terms, the respective terms apply for versions + that are downloaded via these services.

+ +

Překlad. + Čeština: Jaroslav Lichtblau +

+ +

Použité knihovny třetích stran

+

+ * Apache HttpClient (httpclientandroidlib flavour) – Apache License, Version 2.0
+ * iCal4jNew BSD License)
+ * ez-vcardNew BSD License
+ * Simple XML SerializationApache License, Version 2.0
+ * Project LombokMIT License
+ * dnsjavaBSD License

+ ]]>
+ + + Přihlášení s emailovou adresou + Detaily účtu budou automaticky detekovány podle doménového jména. Příkald: mujucet@icloud.com + Přihlášení s URL a uživatelským jménem + Detaily účtu budou automaticky rozpoznány podle URL a uživatelského jména. Nejčastější volba pro vlastní servery. + + Vložte prosím svou emailovou adresu. Její doménová adresa bude použita pro automatické zjištění nastavení služby. + Email: + + + http:// + https:// + + "Pokud nepoužijete šifrované připojení (HTTPS), ostatní budou moci lehce získat vaše přihlašovací údaje, kontakty a události." + Uživatelské jméno: + Kořenová URL (sbírky budou detekovány automaticky): + Preemptivní ověření (doporučeno, ale není kompatibilní s Digest ověřením) + + Heslo: + + DAVdroid: Vybrat sbírky + V tomto umístění není CalDAV-/CardDAV služba dostupná. + Přidat účet + Probíhá komunikace se serverem. Chvilku strpení… + Které sbírky mají být synchronizovány? + Adresáře + Adresář + Kalendáře + Kalendář + Vybrat alespoň jeden adresář (opakovaně tapnout pro odebrání): + Vybrat své kalendáře: + + Detaily účtu + Jméno účtu: + Můj CalDAV/CardDAV účet + Emailová adresa: + "ORGANIZÁTOR událostí; vyžadováno pokud budete přidávat účastníky" + "Použijte svou emailovou adresu jako jméno účtu. Android bude používat tuto hodnotu jako jméno ORGANIZÁTORA událostí které vytvoříte. Nelze mít dva účty se stejným jménem. + pouze pro čtení + + + Obecná nastavení + Ladící nastavení + Vypnout HTTP kompresi + HTTP komprese je vypnuta (ladící mód) + HTTP komprese je použita všude kde možno + Logovat síťový provoz + Veškerý síťový provoz je detailně logován (ladící mód) + Síťový provoz není logován + Nahlásit problém + +
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml new file mode 100644 index 00000000..d77ccd47 --- /dev/null +++ b/app/src/main/res/values-de/strings.xml @@ -0,0 +1,138 @@ + + + + + DAVdroid-Website + Weiter + Hilfe + + HTTP-Fehler: %s + Fehlende Server-Unterstützung: %s + E/A-Fehler: %s + URI ungültig: %s + + + Sync-Konten anzeigen + + Vielen Dank, dass Sie DAVdroid im Play Store erworben haben und das Projekt dadurch unterstützen. + Leider gibt es derzeit zwei Probleme, die vom Play Store verursacht werden:

+ +

1. DAVdroid-Accounts verschwinden nach einem Neustart

+

Möglicherweise verschwinden alle Ihre DAVdroid-Accounts samt Kontakten und Terminen nach einem Neustart + des Geräts. Die Ursache ist ein Fehler in Android, + der zur irrtümlichen Entfernung von Accounts von Bezahlapps bei einem Neustart führt, da die Prüfung auf verwaiste + und damit zu löschende Accounts schon *vor* dem Entschlüsseln der App erfolgt.

+

Betroffene Benutzer*Innen:
+ * alle mit Android 4.1, die DAVdroid über Play Store bezogen haben;
+ * einige mit Android 4.2, die DAVdroid über Play Store bezogen und bestimmte Geräte haben (zB die meisten Samsung-Geräte)

+ +

2. DAVdroid-Accounts verschwinden nach einer DAVdroid-Aktualisierung

+

Möglicherweise verschwinden alle Ihre DAVdroid-Accounts samt Kontakten und Terminen während eines + DAVdroid-Updates, das von Play Store durchgeführt wird. Die Ursache ist ein + anderer Fehler in Android, + der zur irrtümlichen Entfernung von Accounts von Bezahlapps bei der Aktualisierung dieser Apps führt.

+

Betroffene Benutzer*Innen:
+ einige mit Android 4.4.2, die DAVdroid über Play Store bezogen haben (bekannt sind Nexus-Geräte und Moto G)

+ +

Sollten Sie von einem dieser Fehler betroffen sein, installieren + Sie den DAVdroid JB Workaround.

+ ]]>
+ Willkommen bei DAVdroid/%s! + +

DAVdroid ist ein Android 4+-CalDAV/CardDAV-Sync-Adapter. Um ihn zu verwenden, müssen Sie ein DAVdroid-Konto + für Ihren Server hinzufügen. Die Kontakte/Termine werden dann automatisch in beide Richtungen synchronisiert.

+ +

Wenn Sie CyanogenMod benutzen, muss die "Datenschutz"-Funktion für DAVdroid deaktiviert sein, da DAVdroid sonst + keinen Zugriff auf die Kontakte und Kalendar hat und auch nicht synchronisieren kann.

+ +

Weitere Informationen erhalten Sie auf der DAVdroid-Homepage. + Dort finden Sie auch eine Anleitung zum Einrichten. + DAVdroid respektiert Ihre Privatsphäre (siehe Datenschutzrichtlinie).

+ +

Bei Problemen lesen Sie bitte die häufig gestellten Fragen. + Im Falle eines Fehlers, der eindeutig durch DAVdroid verursacht wird, berichten Sie diesen wenn möglich auf + Github Issues, anstatt uns direkt zu kontaktieren oder die App + schlecht zu bewerten.

+ +

Open-Source

+

DAVdroid ist von Anfang an als Open-Source-Projekt ausgelegt. Der Quellcode kann jederzeit selbst kompiliert und + die App unter den Bedingungen der GPLv3 verwendet werden. Der Quellcode ist + auf Github verfügbar, die App kann auch + über F-Droid bezogen werden.

+ +

Es ist jedoch viel Arbeit, die App zu entwickeln und besser zu machen. Daher haben wir uns entschlossen, sie + auch gegen eine kleine Gebühr in die kommerziellen App-Stores zu stellen. Wenn Sie das Projekt unterstützen wollen, können Sie + für DAVdroid spenden oder die App kaufen.

+ +

Lizenz

+

Copyright (c) 2013 – 2014 Ricki Hirner, Bernhard Stockmann (bitfire web engineering), alle Rechte + vorbehalten. Dieses Programm ist freie Software. Sie können es unter den Bedingungen der GNU + General Public License Version 3, wie von der Free Software Foundation veröffentlicht, weitergeben und/oder modifizieren. + Sofern Google Play oder Samsung Store andere Bedingungen benötigen, gelten für über den jeweiligen Markt heruntergeladene + Apps diese Bedingungen.

+ +

Die Übersetzung auf Deutsch wird von den Autoren zur Verfügung gestellt. Übersetzungen für andere Sprachen + wurden von verschiedenen Leuten beigetragen, die in der jeweiligen Sprachversion erwähnt sind.

+ +

Verwendete Bibliotheken

+

+ * Apache HttpClient (mittels httpclientandroidlib) – Apache License, Version 2.0
+ * iCal4jNew BSD License
+ * ez-vcardNew BSD License
+ * Simple XML SerializationApache License, Version 2.0
+ * Project LombokMIT License
+ * dnsjavaBSD License

+ ]]>
+ + + Mit Email-Adresse anmelden + Domänenname wird verwendet, um die Servereinstellungen herauszufinden. Beispiel: myaccount@icloud.com + Mit URL und Benutzername anmelden + Basis-URL und Benutzername werden verwendet, um die Servereinstellungen herauszufinden; z.B. bei einem eigenen Server. + + Geben Sie Ihre Email-Adresse ein. Der Domänenname wird verwendet, um die Servereinstellungen herauszufinden. + Email: + + Ohne Verschlüsselung (HTTPS) können Ihre Zugangsdaten, Kontakte und Termine leicht abgefangen werden. + Benutzername: + Basis-URL (Ordner werden automatisch gefunden): + Präemptive Authentifizierung (empfohlen, aber nicht kompatibel mit Digest-Auth.) + + Passwort: + + DAVdroid: Ordner auswählen + An dieser Adresse konnte kein CalDAV- oder CardDAV-Dienst gefunden werden. + Konto hinzufügen + Daten werden vom Server abgefragt. Bitte warten… + Welche Ordner sollen synchronisiert werden? + Adressbücher + Adressbuch + Kalender + Kalender + Ein oder kein Adressbuch auswählen (nochmal berühren, um abzuwählen): + Kalender zur Synchronisation auswählen: + + Konto-Details + Kontoname: + Mein CalDAV/CardDAV-Konto + Email-Adresse: + "ORGANIZER der von Ihnen angelegten Termine; notwendig für Teilnehmer-Info" + "Verwenden Sie Ihre Email-Adresse als Kontoname, da Android den Kontonamen als ORGANIZER-Feld in Terminen benutzt. Sie können keine zwei Konten mit dem gleichen Namen anlegen. + schreibgeschützt + + + Hilfe zu DAVdroid + + Allgemeine Einstellungen + Einstellungen zur Fehlersuche + HTTP-Komprimierung deaktivieren + HTTP-Komprimierung ist deaktiviert (zur Fehlersuche) + HTTP-Komprimierung wird verwendet, falls möglich + Netzwerkverkehr aufzeichnen + Der gesamte Netzwerkverkehr wird in den Android-Logs mitgeschrieben (zur Fehlersuche) + Netzwerkverkehr wird nicht aufgezeichnet + Problem berichten + +
\ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml new file mode 100644 index 00000000..da7c99c6 --- /dev/null +++ b/app/src/main/res/values-es/strings.xml @@ -0,0 +1,86 @@ + + + + DAVdroid + Ajustes + + + http:// + https:// + + + "Si no usas encriptación (HTTPS), otras personas pueden interceptar fácilmente tus datos de ingreso, contactos y eventos." + Nombre de usuario: + Contraseña: + URL raiz (colecciones autodetectadas): + Siguiente + Ayuda + URL base no válida: + DAVdroid: Selecciona las colecciones + I/O error: %s + URI no válida: %s + Se han perdido capacidades: %s + Ni CalDAV ni CardDAV están disponibles + Añadir cuenta + Consultando el servidor. Espera, por favor… + error HTTP: %s + Qué colecciones quieres sincronizar? + Agendas + Calendarios + Seleciona una agenda (pulsar de nuevo para desmarcar): + Selecciona tus calendarios: + Autentificación preferente (recomendado, pero incompatible con la autentificación de Digest) + + Ayuda de DAVdroid + Administrar las cuentas sincronizadas + Sitio web de DAVdroid + ¡Bienvenido a DAVdroid/%s! + +

DAVdroid es un adaptador de sincronización entre Android 4+ y CalDAV/CardDAV. Para usarlo, simplemente añade una cuenta DAVdroid para tu servidor de CalDAV/CardDAV, así tus contactos y eventos serán sincronizados en ambas direcciones.

+ +

Si usas CyanogenMod, debes desactivar "Privacy Guard" para DAVdroid. De otro modo, DAVdroid no tendrá autorización para acceder y sincronizar tus contactos y eventos.

+ +

Para más información, por favor lee la página de DAVdroid. + También hay una Guía de configuración. DAVdroid respeta tu privacidad, lee nuestra Politica de Privacidad.

+ +

Si tienes problemas, por favor lee las FAQ primero. + Si te encuentras un bug que esté claramente relacionado con DAVdroid, entra en + Github issues en lugar de contactar con nosotros directamente o de darle una mala votación a la aplicación.

+ +

Código abierto

+

DAVdroid ha sido diseñado como un proyecto de código abierto desde sus inicios. Siempre es posible compilar la app por ti mismo y usarla de forma gratuita sin ninguna obligación. El código fuente está disponible en Github, y puedes descargar la app desde F-droid.

+ +

Sin embargo, hay mucho trabajo detrás de la creación de esta aplicación, así que hemos decidido ponerla en las tiendas (Google Play, + Samsung Store) a cambio de un pequeño pago. + Si quieres apoyar este proyecto, por favor piensa en hacer una donación a DAVdroid + o en comprar la app.

+ +

Licencia

+

Copyright (c) 2013 – 2014 Richard Hirner (bitfire web engineering). Todos los derechos reservados. + Este programa y los materiales que la acompañan está disponible bajo las condiciones de la GNU Public License v3.0 que acompaña a esta distribución, y está a tu disposición en http://www.gnu.org/licenses/gpl.html. En lo relativo a Google Play or Samsung requieren otras condiciones, éstas han sido descargadas a través de estos servicios.

+ +

Translations. + Catalanian: @pokoli, + Chinese (simplified): @phy25, + Czech: Jaroslav Lichtblau, + Serbian: @pejakm, + Spanish: @xphnx +

+ +

Librerías third-party usadas

+

+ * iCal4j (Licencia New BSD)
+ * ez-vcard (Licencia New BSD)
+ * Simple XML Serialization (Licencia Apache Version 2.0)
+ * Project Lombok (Licencia MIT)

+ ]]>
+ Detalles de la cuenta + Nombre de la cuenta: + Mi cuenta CalDAV/CardDAV + Dirección de correo: + "ORGANIZADOR de tus eventos; se necesita si se usa información de los asistentes" + "Usa tu dirección de correo electrónico como nombre de cuenta porque Android usará el nombre de cuenta como campo de ORGANIZADOR para los eventos que crees. No puedes tener dos cuentas con el mismo nombre. + +
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 00000000..c6a8aa2f --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,93 @@ + + + + DAVdroid + Paramètres + + + http:// + https:// + + + Si vous n\'utilisez pas de chiffrement(HTTPS), d\'autres personnes peuvent facilement intercepter vos informations de connexion, contacts et événements. + Utilisateur: + Mot de passe: + URL racine(les collections seront autodétectées): + Suivant + Aide + Racine de l\'URL incorrecte: + DAVdroid: Sélectionnez les collections + Erreur I/O: %s + URI incorrecte: %s + Capacités manquantes: %s + Aucun CalDAV ou CardDAV disponible + Ajouter un compte + Interroge le serveur. Patientez svp. + Erreur HTTP: %s + Quelles collections doivent être synchronisées ? + Carnets d\'adresses + Agendas + Choisissez un carnet d\'adresses(toucher à nouveau pour désélectionner): + Choisissez vos agendas: + Authentification préventive(recommandé, mais incompatible avec l\'authentification Digest) + + Aide de DAVdroid + Gérer les comptes synchronisés + Site Web de DAVdroid + Bienvenue dans DAVdroid/%s! + +

DAVdroid est un connecteur de synhronisation entre Android 4+ et CalDAV/CardDAV. Pour l\'utiliser, il suffit d\'ajouter un compte DAVdroid + pour votre serveur CalDAV/CardDAV, et vos contacts/évènements seront synchronisés dans les deux sens.

+ +

Si vous utilisez CyanogenMod, "Privacy Guard" doit être désactivé pour DAVdroid. Sinon DAVdroid ne sera pas en mesure d\'accéder + et synchroniser vos contacts et évènements

+ +

Pour plus d\'informations, visitez la page d\'accueil de DAVdroid. + Il y a Un guide d\'installation également. DAVdroid respecte + votre vie privée, see our Politique de confidentialité.

+ +

En cas de problème, merci de lire la FAQ en premier lieu. + Si vous rencontrer un bug qui est clairement lié à DAVdroid, svp saisissez le sur + Github issues au lieu de nous contacter directement ou d\'attribuer une mauvaise appréciation à l\'application.

+ +

Open-source

+

DAVdroid est conçu depuis le début comme étant open-source. Il est toujours possible de le compiler vous-même l\'application + et l\'utiliser gratuitement sans autre obligation. Le code source est + disponible sur Github, et vous pouvez + télécharger l\'application sur F-droid.

+ +

Néanmoins étant donné que créer cette application nécessite du travail, nous avons décidé de la mettre sur les Stores (Google Play, + Samsung Store) pour un faible coût. + Si vous voulez aider ce projet faites un don à DAVdroid ou achetez le

+ +

License

+

Copyright (c) 2013 – 2014 Ricki Hirner (bitfire web engineering). All rights reserved. + Ce programme et les documents qui l\'accompagnent sont mis à disposition sous les termes de la Licence Public GNU v3.0 qui + accompagne cette distribution, et est disponible à http://www.gnu.org/licenses/gpl.html. En ce qui concerne Google Play ou Samsung Store, les conditions respectives s\'appliquent pour les versions qui sont téléchargées via ces services.

+ +

Translations. + Catalanian: @pokoli, + Chinese (simplified): @phy25, + Czech: Jaroslav Lichtblau, + Serbian: @pejakm, + Spanish: @xphnx +

+ +

Bibliothèques tiers

+

+ * iCal4j (New BSD License)
+ * ez-vcard (New BSD License)
+ * Simple XML Serialization (Apache License, Version 2.0)
+ * Project Lombok (MIT License)

+ ]]>
+ Détails du compte + Nom du compte: + Mon compte CalDAV/CardDAV + Adresse Email: + ORGANISATEUR de vos événements; nécessaire pour l\'information des participants + Utilisez votre adresse email en tant que nom de compte car Android utilise ce nom pour le champ ORGANISATEUR des évènements que vous créez." + Vous ne pouvez pas avoir deux comptes du même nom. + +
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml new file mode 100644 index 00000000..e8d159dd --- /dev/null +++ b/app/src/main/res/values-hu/strings.xml @@ -0,0 +1,147 @@ + + + + + DAVdroid + + DAVdroid web oldal + Tovább + Súgó + + HTTP hiba: %s + Hiányzó szolgáltatás: %s + I/O hiba: %s + Érvénytelen URI: %s + + + Szinkronizációs fiókok kezelése + + Köszönjük, hogy megvásárolta a DAVdroid alkalmazást a Google Play áruházban, támogatva ezzel a projektet. Sajnos, + a Google Play áruházzal két probléma is van:

+

1. A fiókok az eszköz újraindítása után eltűnhetnek

+

Előfordulhat, hogy az eszköz újraindítása után az összes DAVdroid fiók (beleértve hozzájuk tartozó + névjegyeket és eseményeket) eltűnik. Ezt egy + Android szoftverhiba okozza, melynek + következtében a nem ingyenes alkalmazásokhoz tartozó fiókok az eszköz indításakor törlődnek. A hiba hátterében az áll, + hogy a (kódolt) APK fájlok a létrehozó nélküli ("megárvult") fiókok törlése után töltődnek be.

+

Érintett felhasználók:
+ * minden Android 4.1 felhasználó, aki a DAVdroid alkalmazást a Google Play áruházból töltötte le;
+ * egyes Android 4.2 felhasználók, akik a DAVdroid alkalmazást a Google Play áruházból töltötték le, a használt eszköz + típusától függően (például egyes Samsung eszközök felhasználói)

+

2. A fiókok a DAVdroid frissítése után eltűnhetnek

+

Előfordulhat, hogy az összes DAVdroid fiók (beleértve a hozzájuk tartozó névjegyeket és eseményeket) + eltűnik, miután a Google Play áruház frissítette a DAVdroid alkalmazást. Ezt szintén egy, az előzőhöz hasonló, + Android szoftverhiba okozza. +

Érintett felhasználók:
+ * egyes Android 4.4.2 felhasználók, akik a DAVdroid alkalmazást a Google Play áruházból töltötték le, a használt eszköz + típusától függően (például Nexus és Moto G eszközök felhasználói)

+

A probléma a JB DAVdroid gyorsjavítás + telepítésével kiküszöbölhető.

+ ]]>
+ Üdvözöljük a DAVdroid/%s felhasználók között! +

DAVdroid egy Android 4+ CalDAV/CardDAV szinkronizációs adapter. Használatához hozzon létre egy DAVdroid fiókot a használni + kívánt CalDAV/CardDav szerverhez. Ezt követően a névjegyek és események szinkronizálva lesznek, mindkét irányban.

+

További információkat a DAVdroid + honlap tartalmaz. A beállításokkal kapcsolatban a + beállítások oldalon + találhat információkat. A DAVdroid tiszteletben tartja adatait bizalmasságát, részleteket az + adatkezelési nyilatkozat + tartalmaz.

+

Ha Ön CyanogenMod felhasználó, vonja ki a DAVdroidot a "Privacy Guard" hatálya alól, különben a DAVdroid nem fog tudni + hozzáférni a névjegyekhez és eseményekhez.

+

Probléma esetén olvassa el a + GYIK-et. Ha + egyértelműen a DAVdroidhoz köthető hibába ütközik, ahelyett, hogy közvetlenül keresne minket, vagy lepontozná az alkalmazást, + inkább a Problémák oldalon jelentse be.

+

Nyílt forráskód

+

A DAVdroid kezdettől fogva nyílt forráskódú projekt. Bármikor lehetősége van az alkalmazást lefordítani és használni, + mindenféle kötelezettség nélkül. A forráskód elérhető a Githubon, a + lefordított alkalmazás pedig az + F-droidon.

+

Mindemellett, az alkalmazás kifejlesztése sok munkát igényelt, ezért úgy döntöttünk, hogy az alkalmazást más áruházakban + is elérhetővé tesszük, egy szerény díj ellenében. Ha szeretné támogatni ezt a projektet, kérjük, fontolja meg az alkalmazás + megvásárlását vagy közvetlen + támogatását.

+

Licenc

+

Copyright (c) 2013 – 2014 Ricki Hirner (bitfire web engineering). Minden jog fenntartva. + Ez a program és a kapcsolódó anyagok a GNU Public License v3.0 hatálya alatt állnak, amely részét képezi a jelen csomagnak, és + amely elérhető + http://www.gnu.org/licenses/gpl.html oldalon. Amennyiben a Google Play, Samsung + Store, AndroidPit App Center vagy Amazon Appstore áruház eltérő feltételeket szab, akkor annak az áruháznak a szabályai + irányadóak, ahonnan az alkalmazást letöltötte.

+

Fordítások. + cseh: Jaroslav Lichtblau, + katalán: @pokoli, + kínai (egyszerűsített): @phy25, + magyar: Gábor J.Tóth, + szerb: @pejakm, + spanyol: @xphnx. +

+

Harmadik felek által fejlesztett programcsomagok

+

+ * Apache HttpClient ( + httpclientandroidlib változat) – Apache License, Version 2.0
+ * iCal4j + New BSD License
+ * ez-vcard + New BSD License
+ * Simple XML Serialization + Apache License, Version 2.0
+ * Project LombokMIT License

+ ]]>
+ + + Bejelentkezés email cím segítségével + A szolgáltatás részleteinek automatikus detektálása a tartománynév alapján történik. Példa: myaccount@icloud.com + Bejelentkezés URL és felhasználónév segítségével + A szolgáltatás részleteinek automatikus detektálása a kiinduló URL és a felhasználónév alapján történik. Elsősorban privát szolgáltatásoknál ajánlott. + + Kérjük, adja meg email címét. A szolgáltatás részleteinek automatikus detektálása ennek + tartománynév-része alapján fog történni. + Email: + + + http:// + https:// + + "Titkosítás (HTTPS) nélkül a bejelentkezési azonosítókat, névjegyeket és eseményeket könnyen megismerhetik mások is." + Felhasználónév: + Fiók URL (a gyűjtemények detektálása automatikus): + Preemptív authentikáció (ajánlott, de Digest authentikációval nem működik) + + Jelszó: + + DAVdroid: Gyűjtemény kiválasztása + Nincs CalDAV-/CardDAV szolgáltatás a megadott helyen. + Fiók hozzáadása + Kapcsolódás a szerverhez. Egy pillanat… + Melyik gyűjtemények legyenek szinkronizálva? + Címjegyzékek + Címjegyzék + Naptárak + Naptár + Egy címjegyzék választható (a kijelölés visszavonása újbóli érintéssel vagy másik tétel kiválasztásával): + Naptárak kiválasztása: + + További beállítások + A fiók neve: + CalDAV/CardDAV fiók + Email cím: + "Szervező (ORGANIZER mező értéke), résztvevők kezelése esetén" + "Használja az email címet fióknévként, mert később a létrehozandó események szervezőjeként (ORGANIZER mező) az Android ezt fogja használni. Két fiókot nem lehet azonos néven létrehozni. + csak olvasható + + + Általános beállítások + Hibakeresési beállítások + HTTP tömörítés kikapcsolása + HTTP tömörítés kikapcsolva (hibakeresés) + HTTP tömörítés bekapcsolva (ahol csak lehetséges) + Hálózati forgalom naplózása + A teljes hálózati forgalom részletes naplózása (hibakeresés) + A hálózati forgalom naplózása kikapcsolva + Probléma bejelentése + +
\ No newline at end of file diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml new file mode 100644 index 00000000..ebf462b7 --- /dev/null +++ b/app/src/main/res/values-sr/strings.xml @@ -0,0 +1,136 @@ + + + + + ДАВдроид + + ДАВдроид вебсајт + Следеће + Помоћ + + ХТТП грешка: %s + Недостају могућности: %s + У/И грешка: %s + Неисправан УРИ: %s + + + Управљај налозима синхронизације + + Хвала вам што сте купили ДАВдроид преко Google Play сервиса и тиме подржали овај пројекат. Нажалост, постоје два проблема са Google Play сервисом:

+ +

1. Налози могу нестати након поновног покретања

+

Може вам се десити да сви ваши ДАВдроид налози (укључујући контакте и догађаје) нестану након + поновног покретања вашег уређаја. Разлог је грешка у Андроиду + која узрокује уклањање налога плаћених апликација приликом покретања јер се (шифровани) АПК фајлови + учитавају након провере за налоге без апликација.

+

Захваћени корисници:
+ * сви корисници Андроида 4.1 који су инсталирали ДАВдроид са Play Store сервиса;
+ * корисници Андроида 4.2 који су инсталирали ДАВдроид са Play Store сервиса само на неким уређајима (нпр. већина Самсунгових уређаја)

+ +

2. Налози могу да нестану након надоградње ДАВдроида

+

Може вам се десити да сви ваши ДАВдроид налози (укључујући контакте и догађаје) нестану након + надоградње ДАВдроида. Разлог је опет грешка у Андроиду + која узрокује уклањање налога плаћених апликација приликом надоградње из сличног разлога.

+

Захваћени корисници: неки корисници Андроида 4.4.2 који су инсталирали ДАВдроид са Play Store сервиса (Нексус и Мото Г уређаји)

+ +

Ако имате један од наведених проблема, инсталирајте „DAVdroid JB Workaround“.

+ ]]>
+ Добро дошли у ДАВдроид/%s! + +

ДАВдроид (DAVdroid) је Андроид 4+ адаптер синхронизације за КалДАВ/КардДАВ (CalDAV/CardDAV). Да бисте га користили, + само додајте ДАВдроид налог за ваш КалДАВ/КардДАВ сервер и ваши контакти/догађаји ће бити синхронизовани у оба смера.

+ +

За више информација погледајте ДАВдроид вебсајт. + Постоји и водич за поставку. ДАВдроид поштује + вашу приватност, погледајте нашу политику приватности.

+ +

Ако користите Цијаноген Мод, „Privacy Guard“ мора бити онемогућен за ДАВдроид. У супротном, ДАВдроиду неће бити дозвољени + приступ и синхронизација ваших контаката и догађаја.

+ +

У случају проблема, најпре прочитајте најчешћа питања. + Ако вам се деси грешка која је сигурно везана за ДАВдроид, поднесите пријаву на + Гитхабовом пратиоцу уместо да нас контактирате директно или да апликацији дате лошу оцену.

+ +

Отворени код

+

ДАВдроид је испочетка дизајниран као пројекат отвореног кода. Увек је могуће да сами компајлирате + апликацију и да је слободно користите без икаквих обавеза. Изворни код је + доступан на Гитхабу, а можете и + преузети апликацију са Ф-Дроида.

+ +

Међутим, уложено је много рада у ову апликацију па смо одлучили да је ставимо у комерцијалне продавнице за малу накнаду. + Ако желите да подржите овај пројекат, донирајте ДАВдроиду + или га купите.

+ +

Лиценца

+

Ауторско право © 2013 – 2014 Рики Хирнер (Ricki Hirner), Бернхарт Стокман (Bernhard Stockmann) (Битфајер веб инжињеринг). Сва права задржана. + Овај програм и пратећи материјали су доступни под условима ГНУ-ове Јавне Лиценце в3.0 која је приложена, + и доступна на http://www.gnu.org/licenses/gpl.html. Ако сервиси „Google Play“, + „Samsung Store“, „AndroidPit App Center“ или „Amazon Appstore“ захтевају друге услове, исти важе за издања преузета са ових сервиса.

+ +

Превод на српски: @pejakm. Превод на немачки: аутори. Остале преводе + допринели су људи поменути у преводима одговарајућих језика.

+ +

Коришћене друге библиотеке

+

+ * Апачи ХТТП клијент (httpclientandroidlib издање) – Апачи лиценца, издање 2.0
+ * iCal4jНова БСД лиценца)
+ * ez-vcardНова БСД лиценца
+ * Симпле ИксМЛ серијализацијаАпачи лиценца, издање 2.0
+ * Пројекат ЛомбокМИТ лиценца

+ * dnsjavaБСД лиценца

+ ]]>
+ + + Пријавите се адресом е-поште + Детаљи сервиса ће бити аутоматски откривени по имену домена. Пример: mojnalog@icloud.com + Пријавите се УРЛ-ом и корисничким именом + Детаљи сервиса ће бити аутоматски откривени по почетном УРЛ-у и корисничком имену. Углавном се користи за самохостоване сервисе. + + Унесите вашу адресу е-поште. Име домена ће бити коришћено за аутоматско откривање поставки сервиса. + Е-адреса: + + + http:// + https:// + + "Ако не користите шифровање (ХТТПС), други људи вам лако могу пресрести детаље пријаве, контакте и догађаје." + Корисничко име: + Корени УРЛ (збирке ће бити аутоматски откривене): + Превентивна аутентификација (препоручено, али некомпатибилно са Дигест аутентификацијом) + + Лозинка: + + ДАВдроид: Изаберите збирке + Нема доступног КалДАВ/КардДАВ сервиса на овој локацији. + Додај налог + Шаљем упит серверу. Сачекајте… + Које збирке да синхронизујем? + Адресари + Адресар + Календари + Календар + Изаберите један адресар (додирните поново да поништите избор): + Изаберите ваше календаре: + + Детаљи налога + Име налога: + Мој КалДАВ/КардДАВ налог + Е-адреса: + "ОРГАНИЗАТОР ваших догађаја; потребно ако користите податке о учеснику" + "Користите вашу е-адресу као име налога јер Андроид користи име налога за поље ОРГАНИЗАТОР за догађаје које направите. Не можете имати два налога истог имена. + само-за-читање + + + Опште поставке + Поставке проналаска грешака + Онемогући ХТТП компресију + ХТТП компресија је онемогућена (режим проналаска грешака) + ХТТП компресија се користи кад год је могуће + Бележи мрежни саобраћај + Сав мрежни саобраћај се исцрпно бележи у дневник (режим проналаска грешака) + Мрежни саобраћај се не бележи у дневник + Пријави грешку + +
diff --git a/app/src/main/res/values-zh-rcn/strings.xml b/app/src/main/res/values-zh-rcn/strings.xml new file mode 100644 index 00000000..48694a4c --- /dev/null +++ b/app/src/main/res/values-zh-rcn/strings.xml @@ -0,0 +1,126 @@ + + + + + DAVdroid + + DAVdroid 网站 + 继续 + 帮助 + + HTTP 错误: %s + 服务器缺少功能: %s + I/O 错误: %s + URI 无效: %s + + + 管理同步账户 + + 感谢您在 Google Play 上购买 DAVdroid 支持本项目。然而 Google Play 版应用有两个问题:

+ +

1. 账户可能在重启后消失

+

您可能会遇到以下问题:您所有的 DAVdroid 同步账户(包括通讯录和日历)在设备重启后消失。这是由于 Android 系统的一个 BUG,开机时,在检查完孤立账户后才会载入(加密的)程序文件,从而导致了付费应用创建的账户被移除。

+

受影响的用户:
+ * 从 Play 商店安装 DAVdroid 的所有 Android 4.1 用户;
+ * 从 Play 商店安装 DAVdroid 的部分设备(如大部分的三星设备)的 Android 4.2 用户。

+ +

2. 账户可能会在升级 DAVdroid 后消失

+

您可能会遇到以下问题:您所有的 DAVdroid 同步账户(包括通讯录和日历)在使用 Play 商店升级 DAVdroid 后消失。这是 Android 系统的另一个 BUG,由于相似的原因,使得在升级应用后,付费应用创建的账户被移除。

+

受影响的用户:从 Play 商店安装 DAVdroid 的部分 Android 4.4.2 用户(如 Nexus 设备和 Moto G)。

+ +

如果您遇到了这些问题,请安装 + DAVdroid JB 修复程序

+ ]]>
+ 欢迎使用 DAVdroid/%s! + +

DAVdroid 是一个 Android 4+ 的 CalDAV/CardDAV 同步程序。开始使用本程序,只需增加一个 DAVdroid 账户,在其中设置 CalDAV/CardDAV 服务器,您的通讯录和日程就可以被双向同步了。

+ +

更多信息请查阅 DAVdroid 英文主页,其中包括首次使用的配置指南。DAVdroid 尊重您的隐私,请查阅我们的隐私政策

+ +

如果您使用 CyanogenMod 等修改版系统,请停用针对 DAVdroid 的“隐私防护”功能,否则 DAVdroid 无法访问、同步您的通讯录和日程。

+ +

如果您在使用中遇到问题,请先阅读 FAQ。如果您遇到了明显与 DAVdroid 有关的 BUG,请在 Github issues 上提交,不要直接联系我们,甚至给应用差评。

+ +

开源

+

DAVdroid 从一开始就是开源项目。您可以自己编译应用,并可以没有限制地免费使用。源代码 + 存放在 Github 上,您也可以 + 在 F-droid 上下载应用

+ +

然而,编写这个应用是一个大工程,所以我们已经决定把它作为付费应用放在应用商店上,收一笔小费。 + 如果您想支持这个项目,请考虑 给 DAVdroid 捐款 或购买其付费版。

+ +

许可

+

Copyright (c) 2013 – 2014 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. As far as Google Play, Samsung + Store, AndroidPit App Center or Amazon Appstore require other terms, the respective terms apply for versions + that are downloaded via these services.

+ +

Translation for German is provided by the authors. Translation for Chinese Simplified is by @phy25. Translations to other languages have been contributed by + various people which are mentioned in their respective language version.

+ +

使用的第三方程序库

+

+ * Apache HttpClient (httpclientandroidlib flavour) – Apache License, Version 2.0
+ * iCal4jNew BSD License)
+ * ez-vcardNew BSD License
+ * Simple XML SerializationApache License, Version 2.0
+ * Project LombokMIT License
+ * dnsjavaBSD License

+ ]]>
+ + + 使用邮箱地址登录 + 服务器信息会通过域名进行发现。例如: myaccount@icloud.com + 使用 URL 和用户名登录 + 服务器信息会通过 URL 和用户名进行发现。个人搭建的服务通常使用此项。 + + 请输入您的邮箱地址。邮箱的域名会被用来自动发现服务器信息。 + Email: + + + http:// + https:// + + "如果不使用加密连接 (HTTPS),其他人将很容易获取到你的登录信息、通讯录和日程。" + 用户名: + 服务器根地址(集合会自动检测): + 抢先认证模式(推荐使用,但不兼容 Digest 认证方式) + + 密码: + + DAVdroid: 选择同步项 + 找不到可用的 CalDAV-/CardDAV 服务。 + 增加账户 + 正在请求,请稍等… + 需要同步哪些集合? + 通讯录 + 通讯录 + 日历 + 日历 + 最多选择一个通讯录:(再次点按可取消选择) + 选择日历: + + 账户信息 + 账户显示名: + 我的 CalDAV/CardDAV 账户 + Email 地址: + "日程的组织者 (ORGANIZER);如果您使用参与者信息,则必填" + "请使用您的 E-mail 地址作为账户名,因为 Android 会将帐户名用于您创建的日程的参与者 (ORGANIZER) 项。您不能有两个重名的账户。 + 只读 + + + 普通设置 + 调试设置 + 停用 HTTP 压缩 + HTTP 压缩已停用(调试模式) + HTTP 压缩会在可用时使用 + 记录网络传输 + 传输内容会被日志记录(调试模式) + 传输内容不会被日志记录 + 报告问题 + +
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..91a64e75 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,140 @@ + + + + + DAVdroid + + DAVdroid Web site + Next + Help + + HTTP error: %s + Missing capabilities: %s + I/O error: %s + Invalid URI: %s + + + Manage sync accounts + + Thank you for buying DAVdroid via Google Play and thus supporting the project. Unfortunately, there are two issues with Google Play:

+ +

1. Accounts may be gone after a reboot

+

You may encounter the problem that all your DAVdroid accounts (including contacts and events) are gone + after rebooting your device. The reason is a bug in Android + that causes accounts of paid apps to be removed on start-up because the (encrypted) APK files are + loaded after checking for orphaned accounts.

+

Affected users:
+ * all Android 4.1 users who have got DAVdroid from Play Store;
+ * Android 4.2 users who have got DAVdroid from Play Store only with certain devices (for instance, most Samsung devices)

+ +

2. Accounts may be gone after upgrading DAVdroid

+

You may encounter the problem that all your DAVdroid accounts (including contacts and events) when Play Store + updates DAVdroid. The reason is another bug in Android + that causes accounts of paid apps to be removed when upgrading for a similar reason.

+

Affected users: some Android 4.4.2 users who have got DAVdroid from Play Store (known for Nexus devices and Moto G)

+ +

If you\'re affected by one of these bugs, please install the + DAVdroid JB Workaround.

+ ]]>
+ Welcome to DAVdroid/%s! + +

DAVdroid is an Android 4+ sync adapter for CalDAV/CardDAV. To use it, just add a DAVdroid account + for your CalDAV/CardDAV server and your contacts/events will be synchronized in both directions.

+ +

For more information, please see the DAVdroid homepage. + There\'s a Setup guide, too. DAVdroid respects + your privacy, see our Privacy Policy.

+ +

If you use CyanogenMod, "Privacy Guard" must be disabled for DAVdroid. Otherwise, DAVdroid is not allowed to access + and synchronize your contacts and events.

+ +

In case of problems, please read the FAQ first. + If you encounter a bug that is clearly related to DAVdroid, enter it on + Github issues instead of contacting us directly or giving a poor + rating for the app.

+ +

Open-source

+

DAVdroid is designed to be an open-source project from the very first beginning. It is always possible to compile the + app yourself and use it for free without any obligations. The source code is + available on Github, and you can + download the app on F-droid.

+ +

However, it was much work to create this app, so we have decided to put it into the commercial stores for a small fee. + If you want to support this project, please consider donating to DAVdroid + or purchasing it.

+ +

License

+

Copyright (c) 2013 – 2014 Ricki Hirner, Bernhard Stockmann (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. As far as Google Play, Samsung + Store, AndroidPit App Center or Amazon Appstore require other terms, the respective terms apply for versions + that are downloaded via these services.

+ +

Translation for German is provided by the authors. Translations to other languages have been contributed by + various people which are mentioned in their respective language version.

+ +

Used third-party libraries

+

+ * Apache HttpClient (httpclientandroidlib flavour) – Apache License, Version 2.0
+ * iCal4jNew BSD License)
+ * ez-vcardNew BSD License
+ * Simple XML SerializationApache License, Version 2.0
+ * Project LombokMIT License
+ * dnsjavaBSD License

+ ]]>
+ + + Login with email address + Service details will be auto-detected by domain name. Example: myaccount@icloud.com + Login with URL and user name + Service details will be auto-detected by initial URL and user name. Mostly used for self-hosted services. + + Please enter your email address. Its domain name will be used to auto-detect service settings. + Email: + + + http:// + https:// + + "If you don't use encryption (HTTPS), other people may easily intercept your login details, contacts and events." + User name: + Base URL (collections will be auto-detected): + Preemptive authentication (recommended, but incompatible with Digest auth) + + Password: + + DAVdroid: Select collections + No CalDAV-/CardDAV service is available at this location. + Add account + Querying server. Please wait… + Which collections shall be synchronized? + Address books + Address book + Calendars + Calendar + Select up to one address book (tap again to unselect): + Select your calendars: + + Account details + Account name: + My CalDAV/CardDAV Account + Email address: + "ORGANIZER of your events; required if you use attendee info" + "Use your email address as account name because Android will use the account name as ORGANIZER field for events you create. You can't have two accounts with the same name. + read-only + + + General settings + Debug settings + Disable HTTP compression + HTTP compression is disabled (debug mode) + HTTP compression is used whenever possible + Log network traffic + All network traffic is being logged verbosely (debug mode) + Network traffic is not being logged + Report an issue + +
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..a9661327 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/account_authenticator.xml b/app/src/main/res/xml/account_authenticator.xml new file mode 100644 index 00000000..542950e6 --- /dev/null +++ b/app/src/main/res/xml/account_authenticator.xml @@ -0,0 +1,6 @@ + diff --git a/app/src/main/res/xml/account_prefs.xml b/app/src/main/res/xml/account_prefs.xml new file mode 100644 index 00000000..101de536 --- /dev/null +++ b/app/src/main/res/xml/account_prefs.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/contacts.xml b/app/src/main/res/xml/contacts.xml new file mode 100644 index 00000000..10d6eb36 --- /dev/null +++ b/app/src/main/res/xml/contacts.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/general_settings.xml b/app/src/main/res/xml/general_settings.xml new file mode 100644 index 00000000..be95c323 --- /dev/null +++ b/app/src/main/res/xml/general_settings.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/sync_calendars.xml b/app/src/main/res/xml/sync_calendars.xml new file mode 100644 index 00000000..d2ac9b79 --- /dev/null +++ b/app/src/main/res/xml/sync_calendars.xml @@ -0,0 +1,7 @@ + diff --git a/app/src/main/res/xml/sync_contacts.xml b/app/src/main/res/xml/sync_contacts.xml new file mode 100644 index 00000000..3a35ac92 --- /dev/null +++ b/app/src/main/res/xml/sync_contacts.xml @@ -0,0 +1,7 @@ + diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..d6af282e --- /dev/null +++ b/build.gradle @@ -0,0 +1,16 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.0.0' + } +} + +allprojects { + repositories { + jcenter() + mavenCentral() + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..8c0fb64a8698b08ecc4158d828ca593c4928e9dd GIT binary patch literal 49896 zcmagFb986H(k`5d^NVfUwr$(C?M#x1ZQHiZiEVpg+jrjgoQrerx!>1o_ul)D>ebz~ zs=Mmxr&>W81QY-S1PKWQ%N-;H^tS;2*XwVA`dej1RRn1z<;3VgfE4~kaG`A%QSPsR z#ovnZe+tS9%1MfeDyz`RirvdjPRK~p(#^q2(^5@O&NM19EHdvN-A&StN>0g6QA^VN z0Gx%Gq#PD$QMRFzmK+utjS^Y1F0e8&u&^=w5K<;4Rz|i3A=o|IKLY+g`iK6vfr9?+ z-`>gmU&i?FGSL5&F?TXFu`&Js6h;15QFkXp2M1H9|Eq~bpov-GU(uz%mH0n55wUl- zv#~ccAz`F5wlQ>e_KlJS3@{)B?^v*EQM=IxLa&76^y51a((wq|2-`qON>+4dLc{Oo z51}}o^Zen(oAjxDK7b++9_Yg`67p$bPo3~BCpGM7uAWmvIhWc5Gi+gQZ|Pwa-Gll@<1xmcPy z|NZmu6m)g5Ftu~BG&Xdxclw7Cij{xbBMBn-LMII#Slp`AElb&2^Hw+w>(3crLH!;I zN+Vk$D+wP1#^!MDCiad@vM>H#6+`Ct#~6VHL4lzmy;lSdk>`z6)=>Wh15Q2)dQtGqvn0vJU@+(B5{MUc*qs4!T+V=q=wy)<6$~ z!G>e_4dN@lGeF_$q9`Ju6Ncb*x?O7=l{anm7Eahuj_6lA{*#Gv*TaJclevPVbbVYu z(NY?5q+xxbO6%g1xF0r@Ix8fJ~u)VRUp`S%&rN$&e!Od`~s+64J z5*)*WSi*i{k%JjMSIN#X;jC{HG$-^iX+5f5BGOIHWAl*%15Z#!xntpk($-EGKCzKa zT7{siZ9;4TICsWQ$pu&wKZQTCvpI$Xvzwxoi+XkkpeE&&kFb!B?h2hi%^YlXt|-@5 zHJ~%AN!g_^tmn1?HSm^|gCE#!GRtK2(L{9pL#hp0xh zME}|DB>(5)`iE7CM)&_+S}-Bslc#@B5W4_+k4Cp$l>iVyg$KP>CN?SVGZ(&02>iZK zB<^HP$g$Lq*L$BWd?2(F?-MUbNWTJVQdW7$#8a|k_30#vHAD1Z{c#p;bETk0VnU5A zBgLe2HFJ3032$G<`m*OB!KM$*sdM20jm)It5OSru@tXpK5LT>#8)N!*skNu1$TpIw zufjjdp#lyH5bZ%|Iuo|iu9vG1HrIVWLH>278xo>aVBkPN3V$~!=KnlXQ4eDqS7%E% zQ!z^$Q$b^6Q)g#cLpwur(|<0gWHo6A6jc;n`t(V9T;LzTAU{IAu*uEQ%Ort1k+Kn+f_N`9|bxYC+~Z1 zCC1UCWv*Orx$_@ydv9mIe(liLfOr7mhbV@tKw{6)q^1DH1nmvZ0cj215R<~&I<4S| zgnr;9Cdjqpz#o8i0CQjtl`}{c*P)aSdH|abxGdrR)-3z+02-eX(k*B)Uqv6~^nh** z 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..aec99730 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/import-summary.txt b/import-summary.txt new file mode 100644 index 00000000..78d25e5f --- /dev/null +++ b/import-summary.txt @@ -0,0 +1,741 @@ +ECLIPSE ANDROID PROJECT IMPORT SUMMARY +====================================== + +Ignored Files: +-------------- +The following files were *not* copied into the new Gradle project; you +should evaluate whether these are still needed in your project and if +so manually move them: + +* .gitignore +* .idea/ +* .idea/.name +* .idea/compiler.xml +* .idea/copyright/ +* .idea/copyright/profiles_settings.xml +* .idea/davdroid.iml +* .idea/encodings.xml +* .idea/misc.xml +* .idea/modules.xml +* .idea/scopes/ +* .idea/scopes/scope_settings.xml +* .idea/vcs.xml +* .idea/workspace.xml +* CONTRIBUTING.md +* COPYING +* README.md +* build.xml +* compile-libs/ +* compile-libs/lombok.jar +* doc/ +* doc/NIST.SP.800-52r1.pdf +* doc/how_davdroid_works.svgz +* doc/javadoc/ +* doc/javadoc/allclasses-frame.html +* doc/javadoc/allclasses-noframe.html +* doc/javadoc/at/ +* doc/javadoc/at/bitfire/ +* doc/javadoc/at/bitfire/davdroid/ +* doc/javadoc/at/bitfire/davdroid/ArrayUtils.html +* doc/javadoc/at/bitfire/davdroid/Constants.html +* doc/javadoc/at/bitfire/davdroid/MainActivity.html +* doc/javadoc/at/bitfire/davdroid/URIUtils.html +* doc/javadoc/at/bitfire/davdroid/class-use/ +* doc/javadoc/at/bitfire/davdroid/class-use/ArrayUtils.html +* doc/javadoc/at/bitfire/davdroid/class-use/Constants.html +* doc/javadoc/at/bitfire/davdroid/class-use/MainActivity.html +* doc/javadoc/at/bitfire/davdroid/class-use/URIUtils.html +* doc/javadoc/at/bitfire/davdroid/package-frame.html +* doc/javadoc/at/bitfire/davdroid/package-summary.html +* doc/javadoc/at/bitfire/davdroid/package-tree.html +* doc/javadoc/at/bitfire/davdroid/package-use.html +* doc/javadoc/at/bitfire/davdroid/resource/ +* doc/javadoc/at/bitfire/davdroid/resource/CalDavCalendar.html +* doc/javadoc/at/bitfire/davdroid/resource/CardDavAddressBook.html +* doc/javadoc/at/bitfire/davdroid/resource/Contact.html +* doc/javadoc/at/bitfire/davdroid/resource/Event.html +* doc/javadoc/at/bitfire/davdroid/resource/InvalidResourceException.html +* doc/javadoc/at/bitfire/davdroid/resource/LocalAddressBook.html +* doc/javadoc/at/bitfire/davdroid/resource/LocalCalendar.html +* doc/javadoc/at/bitfire/davdroid/resource/LocalCollection.html +* doc/javadoc/at/bitfire/davdroid/resource/LocalStorageException.html +* doc/javadoc/at/bitfire/davdroid/resource/RecordNotFoundException.html +* doc/javadoc/at/bitfire/davdroid/resource/RemoteCollection.html +* doc/javadoc/at/bitfire/davdroid/resource/Resource.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/ +* doc/javadoc/at/bitfire/davdroid/resource/class-use/CalDavCalendar.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/CardDavAddressBook.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/Contact.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/Event.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/InvalidResourceException.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/LocalAddressBook.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/LocalCalendar.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/LocalCollection.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/LocalStorageException.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/RecordNotFoundException.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/RemoteCollection.html +* doc/javadoc/at/bitfire/davdroid/resource/class-use/Resource.html +* doc/javadoc/at/bitfire/davdroid/resource/package-frame.html +* doc/javadoc/at/bitfire/davdroid/resource/package-summary.html +* doc/javadoc/at/bitfire/davdroid/resource/package-tree.html +* doc/javadoc/at/bitfire/davdroid/resource/package-use.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/ +* doc/javadoc/at/bitfire/davdroid/syncadapter/AccountAuthenticatorService.AccountAuthenticator.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/AccountAuthenticatorService.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/AccountDetailsFragment.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/AccountSettings.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/AddAccountActivity.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.SyncAdapter.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/CalendarsSyncAdapterService.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.ContactsSyncAdapter.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/DavSyncAdapter.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/EnterCredentialsFragment.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/GeneralSettingsActivity.GeneralSettingsFragment.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/GeneralSettingsActivity.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/QueryServerDialogFragment.ServerInfoLoader.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/QueryServerDialogFragment.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/SelectCollectionsAdapter.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/SelectCollectionsFragment.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/ServerInfo.ResourceInfo.Type.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/ServerInfo.ResourceInfo.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/ServerInfo.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/SyncManager.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/WebDavResourceAdapter.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/ +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/AccountAuthenticatorService.AccountAuthenticator.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/AccountAuthenticatorService.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/AccountDetailsFragment.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/AccountSettings.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/AddAccountActivity.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/CalendarsSyncAdapterService.SyncAdapter.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/CalendarsSyncAdapterService.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/ContactsSyncAdapterService.ContactsSyncAdapter.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/ContactsSyncAdapterService.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/DavSyncAdapter.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/EnterCredentialsFragment.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/GeneralSettingsActivity.GeneralSettingsFragment.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/GeneralSettingsActivity.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/QueryServerDialogFragment.ServerInfoLoader.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/QueryServerDialogFragment.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/SelectCollectionsAdapter.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/SelectCollectionsFragment.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/ServerInfo.ResourceInfo.Type.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/ServerInfo.ResourceInfo.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/ServerInfo.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/SyncManager.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/class-use/WebDavResourceAdapter.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/package-frame.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/package-summary.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/package-tree.html +* doc/javadoc/at/bitfire/davdroid/syncadapter/package-use.html +* doc/javadoc/at/bitfire/davdroid/webdav/ +* doc/javadoc/at/bitfire/davdroid/webdav/DavAddressbookMultiget.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavCalendarMultiget.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavException.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavHref.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavHttpClient.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavHttpRequestRetryHandler.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavIncapableException.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavMultiget.Type.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavMultiget.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavMultistatus.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavNoContentException.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavNoMultiStatusException.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavAddressbookHomeSet.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavCalendarHomeSet.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavCurrentUserPrincipal.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropAddressData.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropAddressbookDescription.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropCalendarColor.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropCalendarData.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropCalendarDescription.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropCalendarTimezone.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropComp.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropDisplayName.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropGetCTag.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropGetETag.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropPrivilege.PrivAll.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropPrivilege.PrivBind.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropPrivilege.PrivUnbind.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropPrivilege.PrivWrite.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropPrivilege.PrivWriteContent.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropPrivilege.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropResourceType.Addressbook.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropResourceType.Calendar.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.DavPropResourceType.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavProp.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavPropfind.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavPropstat.html +* doc/javadoc/at/bitfire/davdroid/webdav/DavResponse.html +* doc/javadoc/at/bitfire/davdroid/webdav/HttpException.html +* doc/javadoc/at/bitfire/davdroid/webdav/HttpPropfind.Mode.html +* doc/javadoc/at/bitfire/davdroid/webdav/HttpPropfind.html +* doc/javadoc/at/bitfire/davdroid/webdav/HttpReport.html +* doc/javadoc/at/bitfire/davdroid/webdav/NotFoundException.html +* doc/javadoc/at/bitfire/davdroid/webdav/PreconditionFailedException.html +* doc/javadoc/at/bitfire/davdroid/webdav/TlsSniSocketFactory.html +* doc/javadoc/at/bitfire/davdroid/webdav/WebDavResource.Property.html +* doc/javadoc/at/bitfire/davdroid/webdav/WebDavResource.PutMode.html +* doc/javadoc/at/bitfire/davdroid/webdav/WebDavResource.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/ +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavAddressbookMultiget.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavCalendarMultiget.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavException.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavHref.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavHttpClient.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavHttpRequestRetryHandler.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavIncapableException.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavMultiget.Type.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavMultiget.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavMultistatus.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavNoContentException.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavNoMultiStatusException.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavAddressbookHomeSet.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavCalendarHomeSet.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavCurrentUserPrincipal.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropAddressData.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropAddressbookDescription.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropCalendarColor.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropCalendarData.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropCalendarDescription.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropCalendarTimezone.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropComp.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropDisplayName.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropGetCTag.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropGetETag.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropPrivilege.PrivAll.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropPrivilege.PrivBind.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropPrivilege.PrivUnbind.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropPrivilege.PrivWrite.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropPrivilege.PrivWriteContent.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropPrivilege.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropResourceType.Addressbook.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropResourceType.Calendar.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.DavPropResourceType.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavProp.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavPropfind.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavPropstat.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/DavResponse.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/HttpException.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/HttpPropfind.Mode.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/HttpPropfind.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/HttpReport.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/NotFoundException.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/PreconditionFailedException.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/TlsSniSocketFactory.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/WebDavResource.Property.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/WebDavResource.PutMode.html +* doc/javadoc/at/bitfire/davdroid/webdav/class-use/WebDavResource.html +* doc/javadoc/at/bitfire/davdroid/webdav/package-frame.html +* doc/javadoc/at/bitfire/davdroid/webdav/package-summary.html +* doc/javadoc/at/bitfire/davdroid/webdav/package-tree.html +* doc/javadoc/at/bitfire/davdroid/webdav/package-use.html +* doc/javadoc/constant-values.html +* doc/javadoc/deprecated-list.html +* doc/javadoc/help-doc.html +* doc/javadoc/index-files/ +* doc/javadoc/index-files/index-1.html +* doc/javadoc/index-files/index-10.html +* doc/javadoc/index-files/index-11.html +* doc/javadoc/index-files/index-12.html +* doc/javadoc/index-files/index-13.html +* doc/javadoc/index-files/index-14.html +* doc/javadoc/index-files/index-15.html +* doc/javadoc/index-files/index-16.html +* doc/javadoc/index-files/index-17.html +* doc/javadoc/index-files/index-18.html +* doc/javadoc/index-files/index-19.html +* doc/javadoc/index-files/index-2.html +* doc/javadoc/index-files/index-20.html +* doc/javadoc/index-files/index-21.html +* doc/javadoc/index-files/index-22.html +* doc/javadoc/index-files/index-23.html +* doc/javadoc/index-files/index-24.html +* doc/javadoc/index-files/index-3.html +* doc/javadoc/index-files/index-4.html +* doc/javadoc/index-files/index-5.html +* doc/javadoc/index-files/index-6.html +* doc/javadoc/index-files/index-7.html +* doc/javadoc/index-files/index-8.html +* doc/javadoc/index-files/index-9.html +* doc/javadoc/index.html +* doc/javadoc/overview-frame.html +* doc/javadoc/overview-summary.html +* doc/javadoc/overview-tree.html +* doc/javadoc/package-list +* doc/javadoc/resources/ +* doc/javadoc/resources/background.gif +* doc/javadoc/resources/tab.gif +* doc/javadoc/resources/titlebar.gif +* doc/javadoc/resources/titlebar_end.gif +* doc/javadoc/serialized-form.html +* doc/javadoc/stylesheet.css +* doc/rfc3744-webdav-access-control-protocol.txt +* doc/rfc4791-caldav.txt +* doc/rfc4918-webdav.txt +* doc/rfc5397-webdav-current-principal-extension.txt +* doc/rfc5785-well-known-uris.txt +* doc/rfc6352-carddav.txt +* doc/rfc6764-caldav-carddav-service-discovery.txt +* eclipse-libs/ +* eclipse-libs/lombok-api.jar +* private/ +* private/contacts2.db +* proguard-project.txt +* proguard/ +* proguard/dump.txt +* proguard/mapping.txt +* proguard/seeds.txt +* proguard/usage.txt +* target/ +* target/davdroid-0.1.apk +* target/davdroid-0.2-alpha.apk +* target/davdroid-0.3-alpha.apk +* target/davdroid_0_3_3_alpha.apk +* target/davdroid_0_3_4_alpha.apk +* target/davdroid_0_3_5_alpha.apk +* target/davdroid_0_3_6_alpha.apk +* target/davdroid_0_3_7_alpha.apk +* target/davdroid_0_3_8_alpha.apk +* target/davdroid_0_4_1_alpha.apk +* target/davdroid_0_4_2_alpha.apk +* target/davdroid_0_4_3_alpha.apk +* target/davdroid_0_4_4_alpha.apk +* target/davdroid_0_4_alpha.apk +* target/davdroid_0_5_10.apk +* target/davdroid_0_5_10_1.apk +* target/davdroid_0_5_10_2.apk +* target/davdroid_0_5_11.apk +* target/davdroid_0_5_12.apk +* target/davdroid_0_5_13.apk +* target/davdroid_0_5_14.apk +* target/davdroid_0_5_1_alpha.apk +* target/davdroid_0_5_2_alpha.apk +* target/davdroid_0_5_3_alpha.apk +* target/davdroid_0_5_4_alpha.apk +* target/davdroid_0_5_5_alpha.apk +* target/davdroid_0_5_6_alpha.apk +* target/davdroid_0_5_7_alpha.apk +* target/davdroid_0_5_8.apk +* target/davdroid_0_5_8_1.apk +* target/davdroid_0_5_9.apk +* target/davdroid_0_5_alpha.apk +* target/davdroid_0_6.apk +* target/davdroid_0_6_1.apk +* target/davdroid_0_6_2.apk +* target/davdroid_0_6_3.apk +* target/davdroid_0_6_4.apk +* target/davdroid_0_6_5.apk +* target/davdroid_0_6_6.apk +* target/davdroid_0_6_7.apk +* target/davdroid_0_6_7_1.apk +* target/davdroid_0_6_8.apk +* test/ +* test/.classpath +* test/.project +* test/.settings/ +* test/.settings/org.eclipse.jdt.core.prefs +* test/assets/ +* test/assets/all-day-0sec.ics +* test/assets/all-day-10days.ics +* test/assets/all-day-1day.ics +* test/assets/event-on-that-day.ics +* test/assets/impp.vcf +* test/assets/invalid-unknown-properties.vcf +* test/assets/reference.vcf +* test/assets/test.random +* test/assets/vcard3-sample1.vcf +* test/assets/vienna-evolution.ics +* test/bin/ +* test/bin/AndroidManifest.xml +* test/bin/classes.dex +* test/bin/classes/ +* test/bin/classes/at/ +* test/bin/classes/at/bitfire/ +* test/bin/classes/at/bitfire/davdroid/ +* test/bin/classes/at/bitfire/davdroid/resource/ +* test/bin/classes/at/bitfire/davdroid/resource/test/ +* test/bin/classes/at/bitfire/davdroid/resource/test/ContactTest.class +* test/bin/classes/at/bitfire/davdroid/resource/test/EventTest.class +* test/bin/classes/at/bitfire/davdroid/resource/test/LocalCalendarTest.class +* test/bin/classes/at/bitfire/davdroid/syncadapter/ +* test/bin/classes/at/bitfire/davdroid/syncadapter/DavResourceFinderTest.class +* test/bin/classes/at/bitfire/davdroid/test/ +* test/bin/classes/at/bitfire/davdroid/test/ArrayUtilsTest.class +* test/bin/classes/at/bitfire/davdroid/test/BuildConfig.class +* test/bin/classes/at/bitfire/davdroid/test/Constants.class +* test/bin/classes/at/bitfire/davdroid/test/ContactTest.class +* test/bin/classes/at/bitfire/davdroid/test/R$attr.class +* test/bin/classes/at/bitfire/davdroid/test/R$drawable.class +* test/bin/classes/at/bitfire/davdroid/test/R$string.class +* test/bin/classes/at/bitfire/davdroid/test/R.class +* test/bin/classes/at/bitfire/davdroid/test/URLUtilsTest.class +* test/bin/classes/at/bitfire/davdroid/webdav/ +* test/bin/classes/at/bitfire/davdroid/webdav/DavRedirectStrategyTest.class +* test/bin/classes/at/bitfire/davdroid/webdav/TlsSniSocketFactoryTest.class +* test/bin/classes/at/bitfire/davdroid/webdav/WebDavResourceTest.class +* test/bin/davdroidTest.apk +* test/bin/res/ +* test/bin/res/crunch/ +* test/bin/res/crunch/drawable-hdpi/ +* test/bin/res/crunch/drawable-hdpi/ic_launcher.png +* test/bin/res/crunch/drawable-ldpi/ +* test/bin/res/crunch/drawable-ldpi/ic_launcher.png +* test/bin/res/crunch/drawable-mdpi/ +* test/bin/res/crunch/drawable-mdpi/ic_launcher.png +* test/bin/res/crunch/drawable-xhdpi/ +* test/bin/res/crunch/drawable-xhdpi/ic_launcher.png +* test/bin/resources.ap_ +* test/gen/ +* test/gen/at/ +* test/gen/at/bitfire/ +* test/gen/at/bitfire/davdroid/ +* test/gen/at/bitfire/davdroid/test/ +* test/gen/at/bitfire/davdroid/test/BuildConfig.java +* test/gen/at/bitfire/davdroid/test/R.java +* test/proguard-project.txt +* test/project.properties +* test/robohydra/ +* test/robohydra/.gitignore +* test/robohydra/davdroid.conf +* test/robohydra/node_modules/ +* test/robohydra/node_modules/.bin/ +* test/robohydra/node_modules/robohydra/ +* test/robohydra/node_modules/robohydra/.npmignore +* test/robohydra/node_modules/robohydra/ChangeLog +* test/robohydra/node_modules/robohydra/LICENSE +* test/robohydra/node_modules/robohydra/README.md +* test/robohydra/node_modules/robohydra/bin/ +* test/robohydra/node_modules/robohydra/bin/robohydra.js +* test/robohydra/node_modules/robohydra/bin/robohydra.js +* test/robohydra/node_modules/robohydra/examples/ +* test/robohydra/node_modules/robohydra/examples/README.md +* test/robohydra/node_modules/robohydra/examples/custom-types.conf +* test/robohydra/node_modules/robohydra/examples/delayed-proxy.conf +* test/robohydra/node_modules/robohydra/examples/empty-https.conf +* test/robohydra/node_modules/robohydra/examples/empty.conf +* test/robohydra/node_modules/robohydra/examples/external-scenarios.conf +* test/robohydra/node_modules/robohydra/examples/just-scenarios.conf +* test/robohydra/node_modules/robohydra/examples/plugins/ +* test/robohydra/node_modules/robohydra/examples/plugins/basic-scenario/ +* test/robohydra/node_modules/robohydra/examples/plugins/basic-scenario/index.js +* test/robohydra/node_modules/robohydra/examples/plugins/custom-types/ +* test/robohydra/node_modules/robohydra/examples/plugins/custom-types/index.js +* test/robohydra/node_modules/robohydra/examples/plugins/delayed-proxy/ +* test/robohydra/node_modules/robohydra/examples/plugins/delayed-proxy/index.js +* test/robohydra/node_modules/robohydra/examples/plugins/external-scenarios/ +* test/robohydra/node_modules/robohydra/examples/plugins/external-scenarios/fixtures/ +* test/robohydra/node_modules/robohydra/examples/plugins/external-scenarios/fixtures/etc/ +* test/robohydra/node_modules/robohydra/examples/plugins/external-scenarios/fixtures/etc/passwd +* test/robohydra/node_modules/robohydra/examples/plugins/external-scenarios/fixtures/tada.png +* test/robohydra/node_modules/robohydra/examples/plugins/external-scenarios/index.js +* test/robohydra/node_modules/robohydra/examples/plugins/external-scenarios/scenarios/ +* test/robohydra/node_modules/robohydra/examples/plugins/external-scenarios/scenarios/externalScenarioExample.js +* test/robohydra/node_modules/robohydra/examples/plugins/request-finder/ +* test/robohydra/node_modules/robohydra/examples/plugins/request-finder/index.js +* test/robohydra/node_modules/robohydra/examples/plugins/simple-filtering/ +* test/robohydra/node_modules/robohydra/examples/plugins/simple-filtering/index.js +* test/robohydra/node_modules/robohydra/examples/plugins/simple-i18n/ +* test/robohydra/node_modules/robohydra/examples/plugins/simple-i18n/index.js +* test/robohydra/node_modules/robohydra/examples/plugins/simple-streaming/ +* test/robohydra/node_modules/robohydra/examples/plugins/simple-streaming/index.js +* test/robohydra/node_modules/robohydra/examples/plugins/simple-summoner/ +* test/robohydra/node_modules/robohydra/examples/plugins/simple-summoner/index.js +* test/robohydra/node_modules/robohydra/examples/request-finder.conf +* test/robohydra/node_modules/robohydra/examples/simple-filtering.conf +* test/robohydra/node_modules/robohydra/examples/simple-i18n.conf +* test/robohydra/node_modules/robohydra/examples/simple-i18n/ +* test/robohydra/node_modules/robohydra/examples/simple-i18n/foo.en.html +* test/robohydra/node_modules/robohydra/examples/simple-i18n/foo.es.html +* test/robohydra/node_modules/robohydra/examples/simple-i18n/foo.html +* test/robohydra/node_modules/robohydra/examples/simple-i18n/just-french.fr.html +* test/robohydra/node_modules/robohydra/examples/simple-streaming.conf +* test/robohydra/node_modules/robohydra/examples/simple-summoner.conf +* test/robohydra/node_modules/robohydra/examples/test-cert.pem +* test/robohydra/node_modules/robohydra/examples/test-key.pem +* test/robohydra/node_modules/robohydra/lib/ +* test/robohydra/node_modules/robohydra/lib/exceptions.js +* test/robohydra/node_modules/robohydra/lib/heads.js +* test/robohydra/node_modules/robohydra/lib/plugins/ +* test/robohydra/node_modules/robohydra/lib/plugins/admin.js +* test/robohydra/node_modules/robohydra/lib/plugins/static/ +* test/robohydra/node_modules/robohydra/lib/plugins/static/css/ +* test/robohydra/node_modules/robohydra/lib/plugins/static/css/lipstick.css +* test/robohydra/node_modules/robohydra/lib/plugins/static/css/robohydra.css +* test/robohydra/node_modules/robohydra/lib/plugins/static/favicon.ico +* test/robohydra/node_modules/robohydra/lib/plugins/static/img/ +* test/robohydra/node_modules/robohydra/lib/plugins/static/img/glyphicons-halflings-white.png +* test/robohydra/node_modules/robohydra/lib/plugins/static/img/glyphicons-halflings.png +* test/robohydra/node_modules/robohydra/lib/plugins/static/img/robohydra.png +* test/robohydra/node_modules/robohydra/lib/plugins/static/js/ +* test/robohydra/node_modules/robohydra/lib/plugins/static/js/robohydra.js +* test/robohydra/node_modules/robohydra/lib/plugins/templates/ +* test/robohydra/node_modules/robohydra/lib/plugins/templates/_header.ejs +* test/robohydra/node_modules/robohydra/lib/plugins/templates/_scenario-result.ejs +* test/robohydra/node_modules/robohydra/lib/plugins/templates/index.ejs +* test/robohydra/node_modules/robohydra/lib/plugins/templates/layout-main.ejs +* test/robohydra/node_modules/robohydra/lib/plugins/templates/scenario-instructions.ejs +* test/robohydra/node_modules/robohydra/lib/plugins/templates/scenarios.ejs +* test/robohydra/node_modules/robohydra/lib/robohydra.js +* test/robohydra/node_modules/robohydra/lib/robohydrasummoner.js +* test/robohydra/node_modules/robohydra/lib/utils.js +* test/robohydra/node_modules/robohydra/node_modules/ +* test/robohydra/node_modules/robohydra/node_modules/.bin/ +* test/robohydra/node_modules/robohydra/node_modules/commander/ +* test/robohydra/node_modules/robohydra/node_modules/commander/Readme.md +* test/robohydra/node_modules/robohydra/node_modules/commander/index.js +* test/robohydra/node_modules/robohydra/node_modules/commander/package.json +* test/robohydra/node_modules/robohydra/node_modules/ejs/ +* test/robohydra/node_modules/robohydra/node_modules/ejs/.gitmodules +* test/robohydra/node_modules/robohydra/node_modules/ejs/.npmignore +* test/robohydra/node_modules/robohydra/node_modules/ejs/.travis.yml +* test/robohydra/node_modules/robohydra/node_modules/ejs/History.md +* test/robohydra/node_modules/robohydra/node_modules/ejs/Makefile +* test/robohydra/node_modules/robohydra/node_modules/ejs/Readme.md +* test/robohydra/node_modules/robohydra/node_modules/ejs/benchmark.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/ejs.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/ejs.min.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/examples/ +* test/robohydra/node_modules/robohydra/node_modules/ejs/examples/client.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/examples/functions.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/examples/functions.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/examples/list.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/examples/list.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/index.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/lib/ +* test/robohydra/node_modules/robohydra/node_modules/ejs/lib/ejs.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/lib/filters.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/lib/utils.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/package.json +* test/robohydra/node_modules/robohydra/node_modules/ejs/support/ +* test/robohydra/node_modules/robohydra/node_modules/ejs/support/compile.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/ +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/ejs.js +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/ +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/backslash.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/backslash.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/comments.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/comments.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/double-quote.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/double-quote.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/error.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/error.out +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/fail.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/include.css.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/include.css.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/include.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/include.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/includes/ +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/includes/menu-item.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/includes/menu/ +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/includes/menu/item.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/menu.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/menu.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/messed.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/messed.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/newlines.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/newlines.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/no.newlines.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/no.newlines.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/para.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/pet.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/single-quote.ejs +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/single-quote.html +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/style.css +* test/robohydra/node_modules/robohydra/node_modules/ejs/test/fixtures/user.ejs +* test/robohydra/node_modules/robohydra/node_modules/markdown/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/.npmignore +* test/robohydra/node_modules/robohydra/node_modules/markdown/.travis.yml +* test/robohydra/node_modules/robohydra/node_modules/markdown/Changes.markdown +* test/robohydra/node_modules/robohydra/node_modules/markdown/README.markdown +* test/robohydra/node_modules/robohydra/node_modules/markdown/bin/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/bin/md2html.js +* test/robohydra/node_modules/robohydra/node_modules/markdown/bin/md2html.js +* test/robohydra/node_modules/robohydra/node_modules/markdown/lib/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/lib/index.js +* test/robohydra/node_modules/robohydra/node_modules/markdown/lib/markdown.js +* test/robohydra/node_modules/robohydra/node_modules/markdown/markdown-js.sublime-project +* test/robohydra/node_modules/robohydra/node_modules/markdown/markdown-js.sublime-workspace +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/.bin/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/.npmignore +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/LICENSE +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/README.md +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/bin/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/bin/nopt.js +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/bin/nopt.js +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/examples/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/examples/my-program.js +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/lib/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/lib/nopt.js +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/node_modules/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/node_modules/abbrev/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/node_modules/abbrev/LICENSE +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/node_modules/abbrev/README.md +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/node_modules/abbrev/lib/ +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/node_modules/abbrev/lib/abbrev.js +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/node_modules/abbrev/package.json +* test/robohydra/node_modules/robohydra/node_modules/markdown/node_modules/nopt/package.json +* test/robohydra/node_modules/robohydra/node_modules/markdown/package.json +* test/robohydra/node_modules/robohydra/node_modules/markdown/seed.yml +* test/robohydra/node_modules/robohydra/node_modules/mime/ +* test/robohydra/node_modules/robohydra/node_modules/mime/LICENSE +* test/robohydra/node_modules/robohydra/node_modules/mime/README.md +* test/robohydra/node_modules/robohydra/node_modules/mime/mime.js +* test/robohydra/node_modules/robohydra/node_modules/mime/package.json +* test/robohydra/node_modules/robohydra/node_modules/mime/test.js +* test/robohydra/node_modules/robohydra/node_modules/mime/types/ +* test/robohydra/node_modules/robohydra/node_modules/mime/types/mime.types +* test/robohydra/node_modules/robohydra/node_modules/mime/types/node.types +* test/robohydra/node_modules/robohydra/node_modules/qs/ +* test/robohydra/node_modules/robohydra/node_modules/qs/.gitmodules +* test/robohydra/node_modules/robohydra/node_modules/qs/.npmignore +* test/robohydra/node_modules/robohydra/node_modules/qs/Readme.md +* test/robohydra/node_modules/robohydra/node_modules/qs/index.js +* test/robohydra/node_modules/robohydra/node_modules/qs/package.json +* test/robohydra/node_modules/robohydra/package.json +* test/robohydra/node_modules/robohydra/plugins/ +* test/robohydra/node_modules/robohydra/plugins/README.md +* test/robohydra/node_modules/robohydra/plugins/frontend-dev-proxy/ +* test/robohydra/node_modules/robohydra/plugins/frontend-dev-proxy/index.js +* test/robohydra/node_modules/robohydra/plugins/logger/ +* test/robohydra/node_modules/robohydra/plugins/logger/index.js +* test/robohydra/node_modules/robohydra/plugins/no-caching/ +* test/robohydra/node_modules/robohydra/plugins/no-caching/index.js +* test/robohydra/node_modules/robohydra/plugins/replayer/ +* test/robohydra/node_modules/robohydra/plugins/replayer/index.js +* test/robohydra/node_modules/robohydra/test/ +* test/robohydra/node_modules/robohydra/test/adminhead-test.js +* test/robohydra/node_modules/robohydra/test/buster.js +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/ +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/ +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/ +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/ +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/ +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/plugins/ +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/plugins/simple-fixtures/ +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/plugins/simple-fixtures/fixtures/ +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/plugins/simple-fixtures/fixtures/basic.txt +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/plugins/simple-fixtures/fixtures/etc/ +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/plugins/simple-fixtures/fixtures/etc/passwd +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/plugins/simple-fixtures/fixtures/non-ascii.txt +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/plugins/simple-fixtures/index.js +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/plugins/simple-fixtures/tests/ +* test/robohydra/node_modules/robohydra/test/fixture-module-fs/usr/local/share/robohydra/plugins/simple-fixtures/tests/fixtureLoader.js +* test/robohydra/node_modules/robohydra/test/head-test.js +* test/robohydra/node_modules/robohydra/test/helpers.js +* test/robohydra/node_modules/robohydra/test/plugin-fs/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/project/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/project/robohydra-plugins/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/project/robohydra-plugins/definedtwice/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/project/robohydra-plugins/definedtwice/index.js +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/robohydra/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/robohydra/plugins/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/robohydra/plugins/customloadpath/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/robohydra/plugins/customloadpath/index.js +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/robohydra/plugins/definedtwice/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/opt/robohydra/plugins/definedtwice/index.js +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/local/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/local/share/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/local/share/robohydra/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/local/share/robohydra/plugins/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/local/share/robohydra/plugins/definedtwice/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/local/share/robohydra/plugins/definedtwice/index.js +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/definedtwice/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/definedtwice/index.js +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/right-robohydra-test/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/right-robohydra-test/index.js +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/simple-authenticator/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/simple-authenticator/index.js +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/simple/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/simple/index.js +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/url-query-authenticator/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/url-query-authenticator/index.js +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/wrong-fixed-picker/ +* test/robohydra/node_modules/robohydra/test/plugin-fs/usr/share/robohydra/plugins/wrong-fixed-picker/index.js +* test/robohydra/node_modules/robohydra/test/plugins/ +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-conflicting-names/ +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-conflicting-names/index.js +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-conflicting-names/scenarios/ +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-conflicting-names/scenarios/duplicateTestName.js +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-headless/ +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-headless/index.js +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-headless/scenarios/ +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-headless/scenarios/firstTest.js +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-headless/scenarios/secondTest.js +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-mixed/ +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-mixed/index.js +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-mixed/scenarios/ +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-mixed/scenarios/external.js +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-simple/ +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-simple/index.js +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-simple/scenarios/ +* test/robohydra/node_modules/robohydra/test/plugins/external-scenarios-simple/scenarios/firstScenario.js +* test/robohydra/node_modules/robohydra/test/plugins/simple-fixtures/ +* test/robohydra/node_modules/robohydra/test/plugins/simple-fixtures/fixtures/ +* test/robohydra/node_modules/robohydra/test/plugins/simple-fixtures/fixtures/basic.txt +* test/robohydra/node_modules/robohydra/test/plugins/simple-fixtures/fixtures/etc/ +* test/robohydra/node_modules/robohydra/test/plugins/simple-fixtures/fixtures/etc/passwd +* test/robohydra/node_modules/robohydra/test/plugins/simple-fixtures/fixtures/non-ascii.txt +* test/robohydra/node_modules/robohydra/test/plugins/simple-fixtures/index.js +* test/robohydra/node_modules/robohydra/test/plugins/simple-fixtures/scenarios/ +* test/robohydra/node_modules/robohydra/test/plugins/simple-fixtures/scenarios/fixtureLoader.js +* test/robohydra/node_modules/robohydra/test/robohydra-test.js +* test/robohydra/node_modules/robohydra/test/robohydrasummoner-test.js +* test/robohydra/plugins/ +* test/robohydra/plugins/assets/ +* test/robohydra/plugins/assets/index.js +* test/robohydra/plugins/dav-invalid/ +* test/robohydra/plugins/dav-invalid/index.js +* test/robohydra/plugins/dav/ +* test/robohydra/plugins/dav/index.js +* test/robohydra/plugins/headdav.js +* test/robohydra/plugins/redirect/ +* test/robohydra/plugins/redirect/index.js +* test/robohydra/plugins/simple.js +* test/robohydra/run.sh + +Moved Files: +------------ +Android Gradle projects use a different directory structure than ADT +Eclipse projects. Here's how the projects were restructured: + +* AndroidManifest.xml => app/src/main/AndroidManifest.xml +* assets/ => app/src/main/assets/ +* libs/backport-util-concurrent-3.1.jar => app/libs/backport-util-concurrent-3.1.jar +* libs/commons-codec-1.8.jar => app/libs/commons-codec-1.8.jar +* libs/commons-io-2.4.jar => app/libs/commons-io-2.4.jar +* libs/commons-lang-2.6.jar => app/libs/commons-lang-2.6.jar +* libs/commons-logging-1.1.3.jar => app/libs/commons-logging-1.1.3.jar +* libs/ez-vcard-0.9.6.jar => app/libs/ez-vcard-0.9.6.jar +* libs/httpclientandroidlib-1.2.1.jar => app/libs/httpclientandroidlib-1.2.1.jar +* libs/ical4j-1.0.6-davdroid141027.jar => app/libs/ical4j-1.0.6-davdroid141027.jar +* libs/org.xbill.dns_2.1.6.jar => app/libs/org.xbill.dns_2.1.6.jar +* libs/simple-xml-2.7.jar => app/libs/simple-xml-2.7.jar +* lint.xml => app/lint.xml +* res/ => app/src/main/res/ +* src/ => app/src/main/java/ +* test/res/ => app/src/androidTest/res/ +* test/src/ => app/src/androidTest/java/ + +Next Steps: +----------- +You can now build the project. The Gradle project needs network +connectivity to download dependencies. + +Bugs: +----- +If for some reason your project does not build, and you determine that +it is due to a bug or limitation of the Eclipse to Gradle importer, +please file a bug at http://b.android.com with category +Component-Tools. + +(This import summary is for your information only, and can be deleted +after import once you are satisfied with the results.) diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..e7b4def4 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':app'