From 44462885597b188be5153a8a52576c75b64cd742 Mon Sep 17 00:00:00 2001 From: rhodey Date: Sat, 19 Jul 2014 08:28:45 -1000 Subject: [PATCH] big bang --- .gitignore | 20 + BUILDING.md | 53 + README.md | 51 + build.gradle | 1 + flock/.gitignore | 1 + flock/build.gradle | 90 + flock/libs/gradle-witness.jar | Bin 0 -> 21671 bytes .../flock/test/sync/MockMasterCipher.java | 38 + .../HidingCardDavCollectionTest.java | 84 + .../addressbook/HidingCardDavStoreTest.java | 41 + .../calendar/HidingCalDavCollectionTest.java | 131 + .../sync/calendar/HidingCalDavStoreTest.java | 78 + .../flock/test/webdav/DavCollectionTest.java | 46 + .../flock/test/webdav/DavStoreTest.java | 37 + .../flock/test/webdav/DavTestParams.java | 13 + .../webdav/caldav/CalDavCollectionTest.java | 145 ++ .../test/webdav/caldav/CalDavStoreTest.java | 86 + .../webdav/carddav/CardDavCollectionTest.java | 90 + .../test/webdav/carddav/CardDavStoreTest.java | 60 + flock/src/main/AndroidManifest.xml | 199 ++ flock/src/main/artwork/sync_in_progress.png | Bin 0 -> 27919 bytes flock/src/main/assets/flock.store | Bin 0 -> 1892 bytes .../chiralcode/colorpicker/ColorPicker.java | 352 +++ .../colorpicker/ColorPickerPreference.java | 73 + .../wizardpager/wizard/ui/StepPagerStrip.java | 275 +++ .../HoloCircularProgressBar.java | 654 +++++ .../AbstractDavCollectionArrayAdapter.java | 196 ++ .../flock/AbstractMyCollectionsFragment.java | 264 ++ .../flock/AccountAndKeyRequiredActivity.java | 104 + .../flock/AccountAndKeyRequiredFragment.java | 48 + .../AccountContactDetailsListAdapter.java | 125 + .../flock/CalendarCopyService.java | 376 +++ .../ChangeEncryptionPasswordActivity.java | 190 ++ .../ChangeEncryptionPasswordService.java | 344 +++ .../flock/ContactCopyService.java | 273 +++ .../CorrectEncryptionPasswordActivity.java | 145 ++ .../flock/CorrectPasswordActivity.java | 170 ++ .../flock/CorrectPasswordService.java | 252 ++ .../flock/DavAccountHelper.java | 255 ++ .../flock/EditAutoRenewActivity.java | 750 ++++++ .../anhonesteffort/flock/ErrorToaster.java | 374 +++ .../flock/ImportAccountService.java | 100 + .../flock/ImportCalendarsActivity.java | 64 + .../flock/ImportCalendarsFragment.java | 439 ++++ .../flock/ImportContactsActivity.java | 64 + .../flock/ImportContactsFragment.java | 290 +++ .../flock/ImportOtherAccountFragment.java | 237 ++ .../flock/ImportOtherAccountService.java | 196 ++ .../flock/ImportOwsAccountFragment.java | 228 ++ .../flock/ImportOwsAccountService.java | 355 +++ .../flock/IntroductionFragment.java | 67 + .../flock/LocalCalendarListAdapter.java | 151 ++ .../flock/ManageSubscriptionActivity.java | 398 ++++ .../flock/MyAddressbooksActivity.java | 63 + .../flock/MyAddressbooksFragment.java | 361 +++ .../flock/MyCalendarsActivity.java | 64 + .../flock/MyCalendarsFragment.java | 530 +++++ .../flock/PreferencesActivity.java | 228 ++ .../flock/RegisterAccountFragment.java | 236 ++ .../flock/RegisterAccountService.java | 303 +++ .../flock/RemoteAddressbookListAdapter.java | 47 + .../flock/RemoteCalendarListAdapter.java | 159 ++ .../flock/SelectServiceProviderFragment.java | 188 ++ .../flock/SendBitcoinActivity.java | 486 ++++ .../flock/ServerTestsFragment.java | 798 +++++++ .../anhonesteffort/flock/SetupActivity.java | 465 ++++ .../flock/StatusHeaderView.java | 397 ++++ .../flock/UnregisterAccountActivity.java | 255 ++ .../flock/auth/AccountAuthenticator.java | 136 ++ .../auth/AccountAuthenticatorService.java | 37 + .../anhonesteffort/flock/auth/DavAccount.java | 110 + .../flock/crypto/InvalidMacException.java | 39 + .../flock/crypto/KeyHelper.java | 171 ++ .../anhonesteffort/flock/crypto/KeyStore.java | 136 ++ .../anhonesteffort/flock/crypto/KeyUtil.java | 109 + .../flock/crypto/MasterCipher.java | 114 + .../registration/AuthorizationException.java | 31 + .../flock/registration/OwsRegistration.java | 85 + .../flock/registration/RegistrationApi.java | 381 +++ .../RegistrationApiClientException.java | 35 + .../RegistrationApiException.java | 42 + .../ResourceAlreadyExistsException.java | 31 + .../ResourceNotFoundException.java | 31 + .../model/AugmentedFlockAccount.java | 78 + .../registration/model/FlockAccount.java | 184 ++ .../model/FlockCardInformation.java | 107 + .../registration/model/FlockSubscription.java | 108 + .../flock/sync/AbstractDavSyncAdapter.java | 238 ++ .../flock/sync/AbstractDavSyncWorker.java | 623 +++++ .../AbstractLocalComponentCollection.java | 302 +++ .../flock/sync/AbstractSyncScheduler.java | 161 ++ .../flock/sync/AndroidDavClient.java | 62 + .../flock/sync/AppSecureSocketFactory.java | 139 ++ .../flock/sync/HidingDavCollection.java | 71 + .../flock/sync/HidingDavCollectionMixin.java | 118 + .../flock/sync/HidingDavStore.java | 47 + .../anhonesteffort/flock/sync/HidingUtil.java | 79 + .../sync/InvalidLocalComponentException.java | 85 + .../flock/sync/LocalComponentCollection.java | 58 + .../flock/sync/LocalComponentStore.java | 43 + .../anhonesteffort/flock/sync/OwsWebDav.java | 43 + .../anhonesteffort/flock/sync/SyncBooter.java | 42 + .../addressbook/AddressbookSyncScheduler.java | 54 + .../addressbook/AddressbookSyncService.java | 131 + .../addressbook/AddressbookSyncWorker.java | 64 + .../addressbook/ContactCopiedListener.java | 33 + .../sync/addressbook/ContactFactory.java | 1246 ++++++++++ .../addressbook/HidingCardDavCollection.java | 240 ++ .../sync/addressbook/HidingCardDavStore.java | 184 ++ .../addressbook/LocalAddressbookStore.java | 124 + .../addressbook/LocalContactCollection.java | 682 ++++++ .../sync/calendar/CalendarCopiedListener.java | 37 + .../sync/calendar/CalendarSyncWorker.java | 325 +++ .../sync/calendar/CalendarsSyncScheduler.java | 55 + .../sync/calendar/CalendarsSyncService.java | 188 ++ .../flock/sync/calendar/EventFactory.java | 942 ++++++++ .../sync/calendar/HidingCalDavCollection.java | 348 +++ .../sync/calendar/HidingCalDavStore.java | 187 ++ .../sync/calendar/LocalCalendarStore.java | 271 +++ .../sync/calendar/LocalEventCollection.java | 748 ++++++ .../flock/sync/key/KeyProviderStub.java | 62 + .../flock/sync/key/KeySyncScheduler.java | 54 + .../flock/sync/key/KeySyncService.java | 96 + .../flock/sync/key/KeySyncUtil.java | 144 ++ .../flock/sync/key/KeySyncWorker.java | 137 ++ .../org/anhonesteffort/flock/util/Base64.java | 2116 +++++++++++++++++ .../anhonesteffort/flock/util/ColorUtils.java | 243 ++ .../flock/util/PasswordUtil.java | 78 + .../org/anhonesteffort/flock/util/Util.java | 51 + .../AbstractDavComponentCollection.java | 480 ++++ .../webdav/AbstractDavComponentStore.java | 168 ++ .../flock/webdav/ComponentETagPair.java | 45 + .../flock/webdav/DavClient.java | 125 + .../flock/webdav/DavComponentCollection.java | 75 + .../flock/webdav/DavComponentStore.java | 45 + .../flock/webdav/ExtendedMkCol.java | 55 + .../webdav/InvalidComponentException.java | 131 + .../flock/webdav/PropertyParseException.java | 70 + .../flock/webdav/WebDavConstants.java | 71 + .../flock/webdav/caldav/CalDavCollection.java | 403 ++++ .../flock/webdav/caldav/CalDavConstants.java | 132 + .../flock/webdav/caldav/CalDavStore.java | 352 +++ .../webdav/caldav/DavCalendarCollection.java | 65 + .../webdav/carddav/CardDavCollection.java | 207 ++ .../webdav/carddav/CardDavConstants.java | 68 + .../flock/webdav/carddav/CardDavStore.java | 349 +++ .../webdav/carddav/DavContactCollection.java | 42 + .../res/drawable-hdpi/alert_error_dark.png | Bin 0 -> 1416 bytes .../res/drawable-hdpi/alert_warning_light.png | Bin 0 -> 1618 bytes .../main/res/drawable-hdpi/content_copy.png | Bin 0 -> 1373 bytes .../res/drawable-hdpi/content_discard.png | Bin 0 -> 1624 bytes .../main/res/drawable-hdpi/content_edit.png | Bin 0 -> 1880 bytes .../main/res/drawable-hdpi/content_new.png | Bin 0 -> 1157 bytes .../drawable-hdpi/flock_actionbar_icon.png | Bin 0 -> 2529 bytes .../src/main/res/drawable-hdpi/flock_icon.png | Bin 0 -> 6354 bytes ..._check_off_disabled_focused_holo_light.png | Bin 0 -> 395 bytes ...heme_btn_check_off_disabled_holo_light.png | Bin 0 -> 361 bytes ...theme_btn_check_off_focused_holo_light.png | Bin 0 -> 407 bytes .../flocktheme_btn_check_off_holo_light.png | Bin 0 -> 242 bytes ...theme_btn_check_off_pressed_holo_light.png | Bin 0 -> 423 bytes ...n_check_on_disabled_focused_holo_light.png | Bin 0 -> 793 bytes ...theme_btn_check_on_disabled_holo_light.png | Bin 0 -> 624 bytes ...ktheme_btn_check_on_focused_holo_light.png | Bin 0 -> 1687 bytes .../flocktheme_btn_check_on_holo_light.png | Bin 0 -> 1507 bytes ...ktheme_btn_check_on_pressed_holo_light.png | Bin 0 -> 803 bytes ..._default_disabled_focused_holo_light.9.png | Bin 0 -> 317 bytes ...heme_btn_default_disabled_holo_light.9.png | Bin 0 -> 422 bytes ...theme_btn_default_focused_holo_light.9.png | Bin 0 -> 380 bytes ...ktheme_btn_default_normal_holo_light.9.png | Bin 0 -> 321 bytes ...theme_btn_default_pressed_holo_light.9.png | Bin 0 -> 314 bytes ..._radio_off_disabled_focused_holo_light.png | Bin 0 -> 1337 bytes ...heme_btn_radio_off_disabled_holo_light.png | Bin 0 -> 717 bytes ...theme_btn_radio_off_focused_holo_light.png | Bin 0 -> 1428 bytes .../flocktheme_btn_radio_off_holo_light.png | Bin 0 -> 736 bytes ...theme_btn_radio_off_pressed_holo_light.png | Bin 0 -> 1574 bytes ...n_radio_on_disabled_focused_holo_light.png | Bin 0 -> 2293 bytes ...theme_btn_radio_on_disabled_holo_light.png | Bin 0 -> 1169 bytes ...ktheme_btn_radio_on_focused_holo_light.png | Bin 0 -> 2111 bytes .../flocktheme_btn_radio_on_holo_light.png | Bin 0 -> 1447 bytes ...ktheme_btn_radio_on_pressed_holo_light.png | Bin 0 -> 1908 bytes ...ggle_off_disabled_focused_holo_light.9.png | Bin 0 -> 383 bytes ...e_btn_toggle_off_disabled_holo_light.9.png | Bin 0 -> 419 bytes ...me_btn_toggle_off_focused_holo_light.9.png | Bin 0 -> 379 bytes ...eme_btn_toggle_off_normal_holo_light.9.png | Bin 0 -> 459 bytes ...me_btn_toggle_off_pressed_holo_light.9.png | Bin 0 -> 419 bytes ...oggle_on_disabled_focused_holo_light.9.png | Bin 0 -> 409 bytes ...me_btn_toggle_on_disabled_holo_light.9.png | Bin 0 -> 516 bytes ...eme_btn_toggle_on_focused_holo_light.9.png | Bin 0 -> 420 bytes ...heme_btn_toggle_on_normal_holo_light.9.png | Bin 0 -> 521 bytes ...eme_btn_toggle_on_pressed_holo_light.9.png | Bin 0 -> 556 bytes ...ocktheme_fastscroll_thumb_default_holo.png | Bin 0 -> 261 bytes ...ocktheme_fastscroll_thumb_pressed_holo.png | Bin 0 -> 463 bytes .../flocktheme_ic_navigation_drawer.png | Bin 0 -> 122 bytes .../flocktheme_list_activated_holo.9.png | Bin 0 -> 115 bytes .../flocktheme_list_focused_holo.9.png | Bin 0 -> 139 bytes .../flocktheme_list_longpressed_holo.9.png | Bin 0 -> 115 bytes .../flocktheme_list_pressed_holo_light.9.png | Bin 0 -> 115 bytes ...me_list_selector_disabled_holo_light.9.png | Bin 0 -> 189 bytes .../flocktheme_progress_bg_holo_light.9.png | Bin 0 -> 106 bytes ...ocktheme_progress_primary_holo_light.9.png | Bin 0 -> 491 bytes ...ktheme_progress_secondary_holo_light.9.png | Bin 0 -> 143 bytes ...ktheme_progressbar_indeterminate_holo1.png | Bin 0 -> 723 bytes ...ktheme_progressbar_indeterminate_holo2.png | Bin 0 -> 806 bytes ...ktheme_progressbar_indeterminate_holo3.png | Bin 0 -> 908 bytes ...ktheme_progressbar_indeterminate_holo4.png | Bin 0 -> 951 bytes ...ktheme_progressbar_indeterminate_holo5.png | Bin 0 -> 837 bytes ...ktheme_progressbar_indeterminate_holo6.png | Bin 0 -> 955 bytes ...ktheme_progressbar_indeterminate_holo7.png | Bin 0 -> 755 bytes ...ktheme_progressbar_indeterminate_holo8.png | Bin 0 -> 827 bytes ...cktheme_scrubber_control_disabled_holo.png | Bin 0 -> 851 bytes ...ocktheme_scrubber_control_focused_holo.png | Bin 0 -> 992 bytes ...locktheme_scrubber_control_normal_holo.png | Bin 0 -> 1244 bytes ...ocktheme_scrubber_control_pressed_holo.png | Bin 0 -> 1675 bytes .../flocktheme_scrubber_primary_holo.9.png | Bin 0 -> 143 bytes .../flocktheme_scrubber_secondary_holo.9.png | Bin 0 -> 143 bytes ...flocktheme_scrubber_track_holo_light.9.png | Bin 0 -> 169 bytes ...locktheme_spinner_default_holo_light.9.png | Bin 0 -> 421 bytes ...ocktheme_spinner_disabled_holo_light.9.png | Bin 0 -> 378 bytes ...locktheme_spinner_focused_holo_light.9.png | Bin 0 -> 521 bytes ...locktheme_spinner_pressed_holo_light.9.png | Bin 0 -> 627 bytes ...ktheme_switch_bg_disabled_holo_light.9.png | Bin 0 -> 249 bytes ...cktheme_switch_bg_focused_holo_light.9.png | Bin 0 -> 217 bytes .../flocktheme_switch_bg_holo_light.9.png | Bin 0 -> 182 bytes ...me_switch_thumb_activated_holo_light.9.png | Bin 0 -> 430 bytes ...eme_switch_thumb_disabled_holo_light.9.png | Bin 0 -> 535 bytes .../flocktheme_switch_thumb_holo_light.9.png | Bin 0 -> 559 bytes ...heme_switch_thumb_pressed_holo_light.9.png | Bin 0 -> 469 bytes .../flocktheme_text_select_handle_left.png | Bin 0 -> 1356 bytes .../flocktheme_text_select_handle_middle.png | Bin 0 -> 1454 bytes .../flocktheme_text_select_handle_right.png | Bin 0 -> 1389 bytes ...theme_textfield_activated_holo_light.9.png | Bin 0 -> 194 bytes ...cktheme_textfield_default_holo_light.9.png | Bin 0 -> 187 bytes ...extfield_disabled_focused_holo_light.9.png | Bin 0 -> 1208 bytes ...ktheme_textfield_disabled_holo_light.9.png | Bin 0 -> 1116 bytes ...cktheme_textfield_focused_holo_light.9.png | Bin 0 -> 288 bytes .../main/res/drawable-hdpi/icon_calendar.png | Bin 0 -> 751 bytes .../src/main/res/drawable-hdpi/icon_card.png | Bin 0 -> 816 bytes .../src/main/res/drawable-hdpi/icon_lock.png | Bin 0 -> 908 bytes .../res/drawable-mdpi/alert_error_dark.png | Bin 0 -> 1285 bytes .../res/drawable-mdpi/alert_warning_light.png | Bin 0 -> 1362 bytes .../main/res/drawable-mdpi/bitcoin_logo.png | Bin 0 -> 13005 bytes .../main/res/drawable-mdpi/content_copy.png | Bin 0 -> 1321 bytes .../res/drawable-mdpi/content_discard.png | Bin 0 -> 1359 bytes .../main/res/drawable-mdpi/content_edit.png | Bin 0 -> 1506 bytes .../main/res/drawable-mdpi/content_new.png | Bin 0 -> 1099 bytes .../drawable-mdpi/flock_actionbar_icon.png | Bin 0 -> 1825 bytes .../src/main/res/drawable-mdpi/flock_icon.png | Bin 0 -> 3691 bytes ..._check_off_disabled_focused_holo_light.png | Bin 0 -> 294 bytes ...heme_btn_check_off_disabled_holo_light.png | Bin 0 -> 329 bytes ...theme_btn_check_off_focused_holo_light.png | Bin 0 -> 317 bytes .../flocktheme_btn_check_off_holo_light.png | Bin 0 -> 183 bytes ...theme_btn_check_off_pressed_holo_light.png | Bin 0 -> 289 bytes ...n_check_on_disabled_focused_holo_light.png | Bin 0 -> 579 bytes ...theme_btn_check_on_disabled_holo_light.png | Bin 0 -> 508 bytes ...ktheme_btn_check_on_focused_holo_light.png | Bin 0 -> 1094 bytes .../flocktheme_btn_check_on_holo_light.png | Bin 0 -> 999 bytes ...ktheme_btn_check_on_pressed_holo_light.png | Bin 0 -> 554 bytes ..._default_disabled_focused_holo_light.9.png | Bin 0 -> 225 bytes ...heme_btn_default_disabled_holo_light.9.png | Bin 0 -> 313 bytes ...theme_btn_default_focused_holo_light.9.png | Bin 0 -> 283 bytes ...ktheme_btn_default_normal_holo_light.9.png | Bin 0 -> 247 bytes ...theme_btn_default_pressed_holo_light.9.png | Bin 0 -> 245 bytes ..._radio_off_disabled_focused_holo_light.png | Bin 0 -> 760 bytes ...heme_btn_radio_off_disabled_holo_light.png | Bin 0 -> 479 bytes ...theme_btn_radio_off_focused_holo_light.png | Bin 0 -> 794 bytes .../flocktheme_btn_radio_off_holo_light.png | Bin 0 -> 451 bytes ...theme_btn_radio_off_pressed_holo_light.png | Bin 0 -> 962 bytes ...n_radio_on_disabled_focused_holo_light.png | Bin 0 -> 1228 bytes ...theme_btn_radio_on_disabled_holo_light.png | Bin 0 -> 736 bytes ...ktheme_btn_radio_on_focused_holo_light.png | Bin 0 -> 1179 bytes .../flocktheme_btn_radio_on_holo_light.png | Bin 0 -> 839 bytes ...ktheme_btn_radio_on_pressed_holo_light.png | Bin 0 -> 1118 bytes ...ggle_off_disabled_focused_holo_light.9.png | Bin 0 -> 329 bytes ...e_btn_toggle_off_disabled_holo_light.9.png | Bin 0 -> 331 bytes ...me_btn_toggle_off_focused_holo_light.9.png | Bin 0 -> 366 bytes ...eme_btn_toggle_off_normal_holo_light.9.png | Bin 0 -> 344 bytes ...me_btn_toggle_off_pressed_holo_light.9.png | Bin 0 -> 298 bytes ...oggle_on_disabled_focused_holo_light.9.png | Bin 0 -> 299 bytes ...me_btn_toggle_on_disabled_holo_light.9.png | Bin 0 -> 353 bytes ...eme_btn_toggle_on_focused_holo_light.9.png | Bin 0 -> 313 bytes ...heme_btn_toggle_on_normal_holo_light.9.png | Bin 0 -> 399 bytes ...eme_btn_toggle_on_pressed_holo_light.9.png | Bin 0 -> 361 bytes ...ocktheme_fastscroll_thumb_default_holo.png | Bin 0 -> 168 bytes ...ocktheme_fastscroll_thumb_pressed_holo.png | Bin 0 -> 301 bytes .../flocktheme_ic_navigation_drawer.png | Bin 0 -> 106 bytes .../flocktheme_list_activated_holo.9.png | Bin 0 -> 110 bytes .../flocktheme_list_focused_holo.9.png | Bin 0 -> 117 bytes .../flocktheme_list_longpressed_holo.9.png | Bin 0 -> 110 bytes .../flocktheme_list_pressed_holo_light.9.png | Bin 0 -> 110 bytes ...me_list_selector_disabled_holo_light.9.png | Bin 0 -> 171 bytes .../flocktheme_progress_bg_holo_light.9.png | Bin 0 -> 161 bytes ...ocktheme_progress_primary_holo_light.9.png | Bin 0 -> 260 bytes ...ktheme_progress_secondary_holo_light.9.png | Bin 0 -> 134 bytes ...ktheme_progressbar_indeterminate_holo1.png | Bin 0 -> 409 bytes ...ktheme_progressbar_indeterminate_holo2.png | Bin 0 -> 481 bytes ...ktheme_progressbar_indeterminate_holo3.png | Bin 0 -> 533 bytes ...ktheme_progressbar_indeterminate_holo4.png | Bin 0 -> 517 bytes ...ktheme_progressbar_indeterminate_holo5.png | Bin 0 -> 502 bytes ...ktheme_progressbar_indeterminate_holo6.png | Bin 0 -> 533 bytes ...ktheme_progressbar_indeterminate_holo7.png | Bin 0 -> 486 bytes ...ktheme_progressbar_indeterminate_holo8.png | Bin 0 -> 504 bytes ...cktheme_scrubber_control_disabled_holo.png | Bin 0 -> 510 bytes ...ocktheme_scrubber_control_focused_holo.png | Bin 0 -> 619 bytes ...locktheme_scrubber_control_normal_holo.png | Bin 0 -> 749 bytes ...ocktheme_scrubber_control_pressed_holo.png | Bin 0 -> 943 bytes .../flocktheme_scrubber_primary_holo.9.png | Bin 0 -> 133 bytes .../flocktheme_scrubber_secondary_holo.9.png | Bin 0 -> 132 bytes ...flocktheme_scrubber_track_holo_light.9.png | Bin 0 -> 163 bytes ...locktheme_spinner_default_holo_light.9.png | Bin 0 -> 289 bytes ...ocktheme_spinner_disabled_holo_light.9.png | Bin 0 -> 274 bytes ...locktheme_spinner_focused_holo_light.9.png | Bin 0 -> 374 bytes ...locktheme_spinner_pressed_holo_light.9.png | Bin 0 -> 434 bytes ...ktheme_switch_bg_disabled_holo_light.9.png | Bin 0 -> 207 bytes ...cktheme_switch_bg_focused_holo_light.9.png | Bin 0 -> 166 bytes .../flocktheme_switch_bg_holo_light.9.png | Bin 0 -> 163 bytes ...me_switch_thumb_activated_holo_light.9.png | Bin 0 -> 348 bytes ...eme_switch_thumb_disabled_holo_light.9.png | Bin 0 -> 350 bytes .../flocktheme_switch_thumb_holo_light.9.png | Bin 0 -> 362 bytes ...heme_switch_thumb_pressed_holo_light.9.png | Bin 0 -> 297 bytes .../flocktheme_text_select_handle_left.png | Bin 0 -> 861 bytes .../flocktheme_text_select_handle_middle.png | Bin 0 -> 895 bytes .../flocktheme_text_select_handle_right.png | Bin 0 -> 926 bytes ...theme_textfield_activated_holo_light.9.png | Bin 0 -> 163 bytes ...cktheme_textfield_default_holo_light.9.png | Bin 0 -> 151 bytes ...extfield_disabled_focused_holo_light.9.png | Bin 0 -> 1133 bytes ...ktheme_textfield_disabled_holo_light.9.png | Bin 0 -> 1094 bytes ...cktheme_textfield_focused_holo_light.9.png | Bin 0 -> 251 bytes .../main/res/drawable-mdpi/icon_calendar.png | Bin 0 -> 425 bytes .../src/main/res/drawable-mdpi/icon_card.png | Bin 0 -> 410 bytes .../src/main/res/drawable-mdpi/icon_lock.png | Bin 0 -> 432 bytes .../res/drawable-xhdpi/alert_error_dark.png | Bin 0 -> 1554 bytes .../drawable-xhdpi/alert_warning_light.png | Bin 0 -> 1813 bytes .../res/drawable-xhdpi/big_flock_icon.png | Bin 0 -> 193245 bytes .../main/res/drawable-xhdpi/content_copy.png | Bin 0 -> 1438 bytes .../res/drawable-xhdpi/content_discard.png | Bin 0 -> 1848 bytes .../main/res/drawable-xhdpi/content_edit.png | Bin 0 -> 2343 bytes .../main/res/drawable-xhdpi/content_new.png | Bin 0 -> 1225 bytes .../drawable-xhdpi/flock_actionbar_icon.png | Bin 0 -> 3441 bytes .../main/res/drawable-xhdpi/flock_icon.png | Bin 0 -> 10363 bytes ..._check_off_disabled_focused_holo_light.png | Bin 0 -> 466 bytes ...heme_btn_check_off_disabled_holo_light.png | Bin 0 -> 375 bytes ...theme_btn_check_off_focused_holo_light.png | Bin 0 -> 474 bytes .../flocktheme_btn_check_off_holo_light.png | Bin 0 -> 293 bytes ...theme_btn_check_off_pressed_holo_light.png | Bin 0 -> 549 bytes ...n_check_on_disabled_focused_holo_light.png | Bin 0 -> 1045 bytes ...theme_btn_check_on_disabled_holo_light.png | Bin 0 -> 762 bytes ...ktheme_btn_check_on_focused_holo_light.png | Bin 0 -> 2892 bytes .../flocktheme_btn_check_on_holo_light.png | Bin 0 -> 2639 bytes ...ktheme_btn_check_on_pressed_holo_light.png | Bin 0 -> 1134 bytes ..._default_disabled_focused_holo_light.9.png | Bin 0 -> 378 bytes ...heme_btn_default_disabled_holo_light.9.png | Bin 0 -> 541 bytes ...theme_btn_default_focused_holo_light.9.png | Bin 0 -> 487 bytes ...ktheme_btn_default_normal_holo_light.9.png | Bin 0 -> 386 bytes ...theme_btn_default_pressed_holo_light.9.png | Bin 0 -> 373 bytes ..._radio_off_disabled_focused_holo_light.png | Bin 0 -> 1987 bytes ...heme_btn_radio_off_disabled_holo_light.png | Bin 0 -> 919 bytes ...theme_btn_radio_off_focused_holo_light.png | Bin 0 -> 2149 bytes .../flocktheme_btn_radio_off_holo_light.png | Bin 0 -> 950 bytes ...theme_btn_radio_off_pressed_holo_light.png | Bin 0 -> 2235 bytes ...n_radio_on_disabled_focused_holo_light.png | Bin 0 -> 3401 bytes ...theme_btn_radio_on_disabled_holo_light.png | Bin 0 -> 1643 bytes ...ktheme_btn_radio_on_focused_holo_light.png | Bin 0 -> 3535 bytes .../flocktheme_btn_radio_on_holo_light.png | Bin 0 -> 2432 bytes ...ktheme_btn_radio_on_pressed_holo_light.png | Bin 0 -> 2683 bytes ...ggle_off_disabled_focused_holo_light.9.png | Bin 0 -> 409 bytes ...e_btn_toggle_off_disabled_holo_light.9.png | Bin 0 -> 528 bytes ...me_btn_toggle_off_focused_holo_light.9.png | Bin 0 -> 450 bytes ...eme_btn_toggle_off_normal_holo_light.9.png | Bin 0 -> 574 bytes ...me_btn_toggle_off_pressed_holo_light.9.png | Bin 0 -> 546 bytes ...oggle_on_disabled_focused_holo_light.9.png | Bin 0 -> 445 bytes ...me_btn_toggle_on_disabled_holo_light.9.png | Bin 0 -> 633 bytes ...eme_btn_toggle_on_focused_holo_light.9.png | Bin 0 -> 520 bytes ...heme_btn_toggle_on_normal_holo_light.9.png | Bin 0 -> 563 bytes ...eme_btn_toggle_on_pressed_holo_light.9.png | Bin 0 -> 721 bytes ...ocktheme_fastscroll_thumb_default_holo.png | Bin 0 -> 326 bytes ...ocktheme_fastscroll_thumb_pressed_holo.png | Bin 0 -> 605 bytes .../flocktheme_ic_navigation_drawer.png | Bin 0 -> 133 bytes .../flocktheme_list_activated_holo.9.png | Bin 0 -> 121 bytes .../flocktheme_list_focused_holo.9.png | Bin 0 -> 154 bytes .../flocktheme_list_longpressed_holo.9.png | Bin 0 -> 121 bytes .../flocktheme_list_pressed_holo_light.9.png | Bin 0 -> 121 bytes ...me_list_selector_disabled_holo_light.9.png | Bin 0 -> 188 bytes .../flocktheme_progress_bg_holo_light.9.png | Bin 0 -> 178 bytes ...ocktheme_progress_primary_holo_light.9.png | Bin 0 -> 461 bytes ...ktheme_progress_secondary_holo_light.9.png | Bin 0 -> 146 bytes ...ktheme_progressbar_indeterminate_holo1.png | Bin 0 -> 813 bytes ...ktheme_progressbar_indeterminate_holo2.png | Bin 0 -> 984 bytes ...ktheme_progressbar_indeterminate_holo3.png | Bin 0 -> 1181 bytes ...ktheme_progressbar_indeterminate_holo4.png | Bin 0 -> 1129 bytes ...ktheme_progressbar_indeterminate_holo5.png | Bin 0 -> 1076 bytes ...ktheme_progressbar_indeterminate_holo6.png | Bin 0 -> 1186 bytes ...ktheme_progressbar_indeterminate_holo7.png | Bin 0 -> 1063 bytes ...ktheme_progressbar_indeterminate_holo8.png | Bin 0 -> 1049 bytes ...cktheme_scrubber_control_disabled_holo.png | Bin 0 -> 1100 bytes ...ocktheme_scrubber_control_focused_holo.png | Bin 0 -> 1283 bytes ...locktheme_scrubber_control_normal_holo.png | Bin 0 -> 1523 bytes ...ocktheme_scrubber_control_pressed_holo.png | Bin 0 -> 2161 bytes .../flocktheme_scrubber_primary_holo.9.png | Bin 0 -> 149 bytes .../flocktheme_scrubber_secondary_holo.9.png | Bin 0 -> 149 bytes ...flocktheme_scrubber_track_holo_light.9.png | Bin 0 -> 175 bytes ...locktheme_spinner_default_holo_light.9.png | Bin 0 -> 522 bytes ...ocktheme_spinner_disabled_holo_light.9.png | Bin 0 -> 420 bytes ...locktheme_spinner_focused_holo_light.9.png | Bin 0 -> 667 bytes ...locktheme_spinner_pressed_holo_light.9.png | Bin 0 -> 760 bytes ...ktheme_switch_bg_disabled_holo_light.9.png | Bin 0 -> 255 bytes ...cktheme_switch_bg_focused_holo_light.9.png | Bin 0 -> 236 bytes .../flocktheme_switch_bg_holo_light.9.png | Bin 0 -> 229 bytes ...me_switch_thumb_activated_holo_light.9.png | Bin 0 -> 487 bytes ...eme_switch_thumb_disabled_holo_light.9.png | Bin 0 -> 633 bytes .../flocktheme_switch_thumb_holo_light.9.png | Bin 0 -> 687 bytes ...heme_switch_thumb_pressed_holo_light.9.png | Bin 0 -> 584 bytes .../flocktheme_text_select_handle_left.png | Bin 0 -> 1825 bytes .../flocktheme_text_select_handle_middle.png | Bin 0 -> 2005 bytes .../flocktheme_text_select_handle_right.png | Bin 0 -> 1894 bytes ...theme_textfield_activated_holo_light.9.png | Bin 0 -> 228 bytes ...cktheme_textfield_default_holo_light.9.png | Bin 0 -> 220 bytes ...extfield_disabled_focused_holo_light.9.png | Bin 0 -> 1176 bytes ...ktheme_textfield_disabled_holo_light.9.png | Bin 0 -> 1116 bytes ...cktheme_textfield_focused_holo_light.9.png | Bin 0 -> 422 bytes .../main/res/drawable-xhdpi/happy_cloud.png | Bin 0 -> 6134 bytes .../res/drawable-xhdpi/item_focused.9.png | Bin 0 -> 118 bytes .../res/drawable-xhdpi/item_pressed.9.png | Bin 0 -> 163 bytes .../src/main/res/drawable-xhdpi/sad_cloud.png | Bin 0 -> 5308 bytes .../res/drawable-xhdpi/sync_in_progress.png | Bin 0 -> 976 bytes .../drawable-xxhdpi/flock_actionbar_icon.png | Bin 0 -> 5332 bytes ..._check_off_disabled_focused_holo_light.png | Bin 0 -> 453 bytes ...heme_btn_check_off_disabled_holo_light.png | Bin 0 -> 1177 bytes ...theme_btn_check_off_focused_holo_light.png | Bin 0 -> 535 bytes .../flocktheme_btn_check_off_holo_light.png | Bin 0 -> 329 bytes ...theme_btn_check_off_pressed_holo_light.png | Bin 0 -> 598 bytes ...n_check_on_disabled_focused_holo_light.png | Bin 0 -> 1097 bytes ...theme_btn_check_on_disabled_holo_light.png | Bin 0 -> 1574 bytes ...ktheme_btn_check_on_focused_holo_light.png | Bin 0 -> 3789 bytes .../flocktheme_btn_check_on_holo_light.png | Bin 0 -> 3493 bytes ...ktheme_btn_check_on_pressed_holo_light.png | Bin 0 -> 1155 bytes ..._default_disabled_focused_holo_light.9.png | Bin 0 -> 533 bytes ...heme_btn_default_disabled_holo_light.9.png | Bin 0 -> 1643 bytes ...theme_btn_default_focused_holo_light.9.png | Bin 0 -> 842 bytes ...ktheme_btn_default_normal_holo_light.9.png | Bin 0 -> 862 bytes ...theme_btn_default_pressed_holo_light.9.png | Bin 0 -> 816 bytes ..._radio_off_disabled_focused_holo_light.png | Bin 0 -> 2485 bytes ...heme_btn_radio_off_disabled_holo_light.png | Bin 0 -> 2208 bytes ...theme_btn_radio_off_focused_holo_light.png | Bin 0 -> 2694 bytes .../flocktheme_btn_radio_off_holo_light.png | Bin 0 -> 1490 bytes ...theme_btn_radio_off_pressed_holo_light.png | Bin 0 -> 3039 bytes ...n_radio_on_disabled_focused_holo_light.png | Bin 0 -> 4460 bytes ...theme_btn_radio_on_disabled_holo_light.png | Bin 0 -> 3894 bytes ...ktheme_btn_radio_on_focused_holo_light.png | Bin 0 -> 4797 bytes .../flocktheme_btn_radio_on_holo_light.png | Bin 0 -> 3510 bytes ...ktheme_btn_radio_on_pressed_holo_light.png | Bin 0 -> 3544 bytes ...ggle_off_disabled_focused_holo_light.9.png | Bin 0 -> 615 bytes ...e_btn_toggle_off_disabled_holo_light.9.png | Bin 0 -> 1662 bytes ...me_btn_toggle_off_focused_holo_light.9.png | Bin 0 -> 640 bytes ...eme_btn_toggle_off_normal_holo_light.9.png | Bin 0 -> 852 bytes ...me_btn_toggle_off_pressed_holo_light.9.png | Bin 0 -> 869 bytes ...oggle_on_disabled_focused_holo_light.9.png | Bin 0 -> 692 bytes ...me_btn_toggle_on_disabled_holo_light.9.png | Bin 0 -> 1032 bytes ...eme_btn_toggle_on_focused_holo_light.9.png | Bin 0 -> 769 bytes ...heme_btn_toggle_on_normal_holo_light.9.png | Bin 0 -> 845 bytes ...eme_btn_toggle_on_pressed_holo_light.9.png | Bin 0 -> 1197 bytes ...ocktheme_fastscroll_thumb_default_holo.png | Bin 0 -> 604 bytes ...ocktheme_fastscroll_thumb_pressed_holo.png | Bin 0 -> 908 bytes .../flocktheme_ic_navigation_drawer.png | Bin 0 -> 158 bytes .../flocktheme_list_activated_holo.9.png | Bin 0 -> 132 bytes .../flocktheme_list_focused_holo.9.png | Bin 0 -> 157 bytes .../flocktheme_list_longpressed_holo.9.png | Bin 0 -> 133 bytes .../flocktheme_list_pressed_holo_light.9.png | Bin 0 -> 133 bytes ...me_list_selector_disabled_holo_light.9.png | Bin 0 -> 280 bytes .../flocktheme_progress_bg_holo_light.9.png | Bin 0 -> 1084 bytes ...ocktheme_progress_primary_holo_light.9.png | Bin 0 -> 837 bytes ...ktheme_progress_secondary_holo_light.9.png | Bin 0 -> 142 bytes ...ktheme_progressbar_indeterminate_holo1.png | Bin 0 -> 1323 bytes ...ktheme_progressbar_indeterminate_holo2.png | Bin 0 -> 1627 bytes ...ktheme_progressbar_indeterminate_holo3.png | Bin 0 -> 1904 bytes ...ktheme_progressbar_indeterminate_holo4.png | Bin 0 -> 1901 bytes ...ktheme_progressbar_indeterminate_holo5.png | Bin 0 -> 1797 bytes ...ktheme_progressbar_indeterminate_holo6.png | Bin 0 -> 1940 bytes ...ktheme_progressbar_indeterminate_holo7.png | Bin 0 -> 1741 bytes ...ktheme_progressbar_indeterminate_holo8.png | Bin 0 -> 1744 bytes ...cktheme_scrubber_control_disabled_holo.png | Bin 0 -> 1409 bytes ...ocktheme_scrubber_control_focused_holo.png | Bin 0 -> 1939 bytes ...locktheme_scrubber_control_normal_holo.png | Bin 0 -> 2414 bytes ...ocktheme_scrubber_control_pressed_holo.png | Bin 0 -> 2530 bytes .../flocktheme_scrubber_primary_holo.9.png | Bin 0 -> 143 bytes .../flocktheme_scrubber_secondary_holo.9.png | Bin 0 -> 147 bytes ...flocktheme_scrubber_track_holo_light.9.png | Bin 0 -> 1095 bytes ...locktheme_spinner_default_holo_light.9.png | Bin 0 -> 446 bytes ...ocktheme_spinner_disabled_holo_light.9.png | Bin 0 -> 1326 bytes ...locktheme_spinner_focused_holo_light.9.png | Bin 0 -> 669 bytes ...locktheme_spinner_pressed_holo_light.9.png | Bin 0 -> 640 bytes ...ktheme_switch_bg_disabled_holo_light.9.png | Bin 0 -> 1192 bytes ...cktheme_switch_bg_focused_holo_light.9.png | Bin 0 -> 282 bytes .../flocktheme_switch_bg_holo_light.9.png | Bin 0 -> 1186 bytes ...me_switch_thumb_activated_holo_light.9.png | Bin 0 -> 910 bytes ...eme_switch_thumb_disabled_holo_light.9.png | Bin 0 -> 1723 bytes .../flocktheme_switch_thumb_holo_light.9.png | Bin 0 -> 1920 bytes ...heme_switch_thumb_pressed_holo_light.9.png | Bin 0 -> 825 bytes .../flocktheme_text_select_handle_left.png | Bin 0 -> 2717 bytes .../flocktheme_text_select_handle_middle.png | Bin 0 -> 2845 bytes .../flocktheme_text_select_handle_right.png | Bin 0 -> 2527 bytes ...theme_textfield_activated_holo_light.9.png | Bin 0 -> 329 bytes ...cktheme_textfield_default_holo_light.9.png | Bin 0 -> 325 bytes ...extfield_disabled_focused_holo_light.9.png | Bin 0 -> 464 bytes ...ktheme_textfield_disabled_holo_light.9.png | Bin 0 -> 315 bytes ...cktheme_textfield_focused_holo_light.9.png | Bin 0 -> 502 bytes .../drawable-xxxhdpi/flock_actionbar_icon.png | Bin 0 -> 7346 bytes .../main/res/drawable-xxxhdpi/flock_icon.png | Bin 0 -> 28591 bytes ...tion_item_received_triangle_shape_grey.xml | 33 + ...on_item_received_triangle_shape_orange.xml | 33 + .../main/res/drawable/finish_background.xml | 20 + .../src/main/res/drawable/flock_gradient.xml | 9 + ...ktheme_activated_background_holo_light.xml | 20 + .../flocktheme_btn_check_holo_light.xml | 65 + .../flocktheme_btn_default_holo_light.xml | 32 + .../flocktheme_btn_radio_holo_light.xml | 59 + .../flocktheme_btn_toggle_holo_light.xml | 50 + .../flocktheme_edit_text_holo_light.xml | 25 + .../flocktheme_fastscroll_thumb_holo.xml | 20 + .../flocktheme_item_background_holo_light.xml | 26 + ...ector_background_transition_holo_light.xml | 20 + .../flocktheme_list_selector_holo_light.xml | 28 + ...cktheme_progress_horizontal_holo_light.xml | 32 + ...e_progress_horizontal_holo_light_green.xml | 21 + ...eme_progress_horizontal_holo_light_red.xml | 21 + ..._progress_horizontal_holo_light_yellow.xml | 21 + ...ss_indeterminate_horizontal_holo_light.xml | 30 + ...e_scrubber_control_selector_holo_light.xml | 22 + ...crubber_progress_horizontal_holo_light.xml | 28 + ...ocktheme_spinner_background_holo_light.xml | 25 + .../flocktheme_switch_inner_holo_light.xml | 22 + .../flocktheme_switch_track_holo_light.xml | 20 + .../main/res/drawable/rounded_thing_grey.xml | 32 + .../res/drawable/rounded_thing_orange.xml | 32 + .../drawable/selectable_item_background.xml | 22 + .../activity_manage_subscription.xml | 118 + .../res/layout-land/activity_send_bitcoin.xml | 156 ++ .../main/res/layout-land/fragment_intro.xml | 59 + .../fragment_select_sync_provider.xml | 87 + .../activity_send_bitcoin.xml | 157 ++ .../activity_edit_auto_renew.xml | 140 ++ .../layout-sw320dp/activity_send_bitcoin.xml | 151 ++ .../activity_manage_subscription.xml | 120 + .../activity_send_bitcoin.xml | 158 ++ .../layout-sw600dp-land/fragment_intro.xml | 59 + .../fragment_select_sync_provider.xml | 90 + .../activity_edit_auto_renew.xml | 141 ++ .../activity_manage_subscription.xml | 114 + .../layout-sw600dp/activity_send_bitcoin.xml | 152 ++ .../res/layout-sw600dp/fragment_intro.xml | 51 + .../fragment_select_sync_provider.xml | 80 + .../res/layout/activity_edit_auto_renew.xml | 141 ++ .../layout/activity_manage_subscription.xml | 111 + .../main/res/layout/activity_send_bitcoin.xml | 152 ++ .../layout/activity_with_action_button.xml | 53 + .../res/layout/change_encryption_password.xml | 79 + .../layout/correct_encryption_password.xml | 50 + .../src/main/res/layout/correct_password.xml | 50 + .../res/layout/dialog_addressbook_edit.xml | 17 + .../main/res/layout/dialog_calendar_edit.xml | 24 + .../layout/fragment_import_other_account.xml | 77 + .../layout/fragment_import_ows_account.xml | 42 + flock/src/main/res/layout/fragment_intro.xml | 50 + .../layout/fragment_list_sync_collections.xml | 42 + .../layout/fragment_register_ows_account.xml | 101 + .../layout/fragment_select_sync_provider.xml | 77 + .../main/res/layout/fragment_server_tests.xml | 72 + .../main/res/layout/fragment_simple_list.xml | 10 + .../layout/row_account_contact_details.xml | 40 + .../res/layout/row_local_calendar_details.xml | 47 + .../layout/row_remote_addressbook_details.xml | 25 + .../layout/row_remote_calendar_details.xml | 32 + flock/src/main/res/layout/setup_activity.xml | 77 + .../res/layout/simple_fragment_activity.xml | 33 + .../main/res/layout/status_header_view.xml | 22 + .../main/res/layout/unregister_account.xml | 53 + .../main/res/menu/addressbook_list_browse.xml | 2 + .../main/res/menu/calendar_list_browse.xml | 8 + .../main/res/menu/collection_list_delete.xml | 8 + .../main/res/menu/collection_list_edit.xml | 13 + .../src/main/res/menu/manage_subscription.xml | 8 + flock/src/main/res/values/attrs.xml | 36 + flock/src/main/res/values/colors.xml | 17 + .../src/main/res/values/colors_flocktheme.xml | 4 + flock/src/main/res/values/dimens.xml | 9 + flock/src/main/res/values/integers.xml | 16 + flock/src/main/res/values/strings.xml | 363 +++ flock/src/main/res/values/styles.xml | 70 + .../src/main/res/values/styles_flocktheme.xml | 39 + .../src/main/res/values/themes_flocktheme.xml | 69 + .../main/res/xml/account_authenticator.xml | 6 + .../src/main/res/xml/account_preferences.xml | 7 + .../main/res/xml/calendars_syncadapter.xml | 8 + flock/src/main/res/xml/contacts_structure.xml | 93 + .../src/main/res/xml/contacts_syncadapter.xml | 8 + flock/src/main/res/xml/keys_syncadapter.xml | 8 + flock/src/main/res/xml/preferences.xml | 71 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 50557 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 ++ gradlew.bat | 90 + settings.gradle | 1 + 601 files changed, 34654 insertions(+) create mode 100644 .gitignore create mode 100644 BUILDING.md create mode 100644 README.md create mode 100644 build.gradle create mode 100644 flock/.gitignore create mode 100644 flock/build.gradle create mode 100644 flock/libs/gradle-witness.jar create mode 100644 flock/src/androidTest/java/org/anhonesteffort/flock/test/sync/MockMasterCipher.java create mode 100644 flock/src/androidTest/java/org/anhonesteffort/flock/test/sync/addressbook/HidingCardDavCollectionTest.java create mode 100644 flock/src/androidTest/java/org/anhonesteffort/flock/test/sync/addressbook/HidingCardDavStoreTest.java create mode 100644 flock/src/androidTest/java/org/anhonesteffort/flock/test/sync/calendar/HidingCalDavCollectionTest.java create mode 100644 flock/src/androidTest/java/org/anhonesteffort/flock/test/sync/calendar/HidingCalDavStoreTest.java create mode 100644 flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/DavCollectionTest.java create mode 100644 flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/DavStoreTest.java create mode 100644 flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/DavTestParams.java create mode 100644 flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/caldav/CalDavCollectionTest.java create mode 100644 flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/caldav/CalDavStoreTest.java create mode 100644 flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/carddav/CardDavCollectionTest.java create mode 100644 flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/carddav/CardDavStoreTest.java create mode 100644 flock/src/main/AndroidManifest.xml create mode 100644 flock/src/main/artwork/sync_in_progress.png create mode 100644 flock/src/main/assets/flock.store create mode 100644 flock/src/main/java/com/chiralcode/colorpicker/ColorPicker.java create mode 100644 flock/src/main/java/com/chiralcode/colorpicker/ColorPickerPreference.java create mode 100644 flock/src/main/java/com/example/android/wizardpager/wizard/ui/StepPagerStrip.java create mode 100644 flock/src/main/java/de/passsy/holocircularprogressbar/HoloCircularProgressBar.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/AbstractDavCollectionArrayAdapter.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/AbstractMyCollectionsFragment.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/AccountAndKeyRequiredActivity.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/AccountAndKeyRequiredFragment.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/AccountContactDetailsListAdapter.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/CalendarCopyService.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/ChangeEncryptionPasswordActivity.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/ChangeEncryptionPasswordService.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/ContactCopyService.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/CorrectEncryptionPasswordActivity.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/CorrectPasswordActivity.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/CorrectPasswordService.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/DavAccountHelper.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/EditAutoRenewActivity.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/ErrorToaster.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/ImportAccountService.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/ImportCalendarsActivity.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/ImportCalendarsFragment.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/ImportContactsActivity.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/ImportContactsFragment.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/ImportOtherAccountFragment.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/ImportOtherAccountService.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/ImportOwsAccountFragment.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/ImportOwsAccountService.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/IntroductionFragment.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/LocalCalendarListAdapter.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/ManageSubscriptionActivity.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/MyAddressbooksActivity.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/MyAddressbooksFragment.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/MyCalendarsActivity.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/MyCalendarsFragment.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/PreferencesActivity.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/RegisterAccountFragment.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/RegisterAccountService.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/RemoteAddressbookListAdapter.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/RemoteCalendarListAdapter.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/SelectServiceProviderFragment.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/SendBitcoinActivity.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/ServerTestsFragment.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/SetupActivity.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/StatusHeaderView.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/UnregisterAccountActivity.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/auth/AccountAuthenticator.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/auth/AccountAuthenticatorService.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/auth/DavAccount.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/crypto/InvalidMacException.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/crypto/KeyHelper.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/crypto/KeyStore.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/crypto/KeyUtil.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/crypto/MasterCipher.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/registration/AuthorizationException.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/registration/OwsRegistration.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/registration/RegistrationApi.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/registration/RegistrationApiClientException.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/registration/RegistrationApiException.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/registration/ResourceAlreadyExistsException.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/registration/ResourceNotFoundException.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/registration/model/AugmentedFlockAccount.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/registration/model/FlockAccount.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/registration/model/FlockCardInformation.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/registration/model/FlockSubscription.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/AbstractDavSyncAdapter.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/AbstractDavSyncWorker.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/AbstractLocalComponentCollection.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/AbstractSyncScheduler.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/AndroidDavClient.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/AppSecureSocketFactory.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/HidingDavCollection.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/HidingDavCollectionMixin.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/HidingDavStore.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/HidingUtil.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/InvalidLocalComponentException.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/LocalComponentCollection.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/LocalComponentStore.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/OwsWebDav.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/SyncBooter.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/AddressbookSyncScheduler.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/AddressbookSyncService.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/AddressbookSyncWorker.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/ContactCopiedListener.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/ContactFactory.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/HidingCardDavCollection.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/HidingCardDavStore.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/LocalAddressbookStore.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/LocalContactCollection.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/calendar/CalendarCopiedListener.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/calendar/CalendarSyncWorker.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/calendar/CalendarsSyncScheduler.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/calendar/CalendarsSyncService.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/calendar/EventFactory.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/calendar/HidingCalDavCollection.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/calendar/HidingCalDavStore.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/calendar/LocalCalendarStore.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/calendar/LocalEventCollection.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/key/KeyProviderStub.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/key/KeySyncScheduler.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/key/KeySyncService.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/key/KeySyncUtil.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/sync/key/KeySyncWorker.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/util/Base64.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/util/ColorUtils.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/util/PasswordUtil.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/util/Util.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/webdav/AbstractDavComponentCollection.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/webdav/AbstractDavComponentStore.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/webdav/ComponentETagPair.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/webdav/DavClient.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/webdav/DavComponentCollection.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/webdav/DavComponentStore.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/webdav/ExtendedMkCol.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/webdav/InvalidComponentException.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/webdav/PropertyParseException.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/webdav/WebDavConstants.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/webdav/caldav/CalDavCollection.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/webdav/caldav/CalDavConstants.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/webdav/caldav/CalDavStore.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/webdav/caldav/DavCalendarCollection.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/webdav/carddav/CardDavCollection.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/webdav/carddav/CardDavConstants.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/webdav/carddav/CardDavStore.java create mode 100644 flock/src/main/java/org/anhonesteffort/flock/webdav/carddav/DavContactCollection.java create mode 100644 flock/src/main/res/drawable-hdpi/alert_error_dark.png create mode 100644 flock/src/main/res/drawable-hdpi/alert_warning_light.png create mode 100644 flock/src/main/res/drawable-hdpi/content_copy.png create mode 100644 flock/src/main/res/drawable-hdpi/content_discard.png create mode 100644 flock/src/main/res/drawable-hdpi/content_edit.png create mode 100644 flock/src/main/res/drawable-hdpi/content_new.png create mode 100644 flock/src/main/res/drawable-hdpi/flock_actionbar_icon.png create mode 100644 flock/src/main/res/drawable-hdpi/flock_icon.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_check_off_disabled_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_check_off_disabled_holo_light.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_check_off_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_check_off_holo_light.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_check_off_pressed_holo_light.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_check_on_disabled_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_check_on_disabled_holo_light.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_check_on_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_check_on_holo_light.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_check_on_pressed_holo_light.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_default_disabled_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_default_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_default_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_default_normal_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_default_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_radio_off_disabled_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_radio_off_disabled_holo_light.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_radio_off_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_radio_off_holo_light.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_radio_off_pressed_holo_light.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_radio_on_disabled_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_radio_on_disabled_holo_light.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_radio_on_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_radio_on_holo_light.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_radio_on_pressed_holo_light.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_off_disabled_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_off_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_off_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_off_normal_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_off_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_on_disabled_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_on_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_on_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_on_normal_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_on_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_fastscroll_thumb_default_holo.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_fastscroll_thumb_pressed_holo.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_ic_navigation_drawer.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_list_activated_holo.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_list_focused_holo.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_list_longpressed_holo.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_list_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_list_selector_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_progress_bg_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_progress_primary_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_progress_secondary_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_progressbar_indeterminate_holo1.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_progressbar_indeterminate_holo2.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_progressbar_indeterminate_holo3.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_progressbar_indeterminate_holo4.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_progressbar_indeterminate_holo5.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_progressbar_indeterminate_holo6.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_progressbar_indeterminate_holo7.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_progressbar_indeterminate_holo8.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_scrubber_control_disabled_holo.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_scrubber_control_focused_holo.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_scrubber_control_normal_holo.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_scrubber_control_pressed_holo.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_scrubber_primary_holo.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_scrubber_secondary_holo.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_scrubber_track_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_spinner_default_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_spinner_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_spinner_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_spinner_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_switch_bg_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_switch_bg_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_switch_bg_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_switch_thumb_activated_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_switch_thumb_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_switch_thumb_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_switch_thumb_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_text_select_handle_left.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_text_select_handle_middle.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_text_select_handle_right.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_textfield_activated_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_textfield_default_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_textfield_disabled_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_textfield_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-hdpi/flocktheme_textfield_focused_holo_light.9.png create mode 100644 flock/src/main/res/drawable-hdpi/icon_calendar.png create mode 100644 flock/src/main/res/drawable-hdpi/icon_card.png create mode 100644 flock/src/main/res/drawable-hdpi/icon_lock.png create mode 100644 flock/src/main/res/drawable-mdpi/alert_error_dark.png create mode 100644 flock/src/main/res/drawable-mdpi/alert_warning_light.png create mode 100644 flock/src/main/res/drawable-mdpi/bitcoin_logo.png create mode 100644 flock/src/main/res/drawable-mdpi/content_copy.png create mode 100644 flock/src/main/res/drawable-mdpi/content_discard.png create mode 100644 flock/src/main/res/drawable-mdpi/content_edit.png create mode 100644 flock/src/main/res/drawable-mdpi/content_new.png create mode 100644 flock/src/main/res/drawable-mdpi/flock_actionbar_icon.png create mode 100644 flock/src/main/res/drawable-mdpi/flock_icon.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_check_off_disabled_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_check_off_disabled_holo_light.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_check_off_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_check_off_holo_light.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_check_off_pressed_holo_light.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_check_on_disabled_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_check_on_disabled_holo_light.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_check_on_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_check_on_holo_light.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_check_on_pressed_holo_light.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_default_disabled_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_default_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_default_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_default_normal_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_default_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_radio_off_disabled_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_radio_off_disabled_holo_light.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_radio_off_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_radio_off_holo_light.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_radio_off_pressed_holo_light.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_radio_on_disabled_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_radio_on_disabled_holo_light.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_radio_on_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_radio_on_holo_light.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_radio_on_pressed_holo_light.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_off_disabled_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_off_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_off_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_off_normal_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_off_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_on_disabled_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_on_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_on_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_on_normal_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_on_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_fastscroll_thumb_default_holo.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_fastscroll_thumb_pressed_holo.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_ic_navigation_drawer.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_list_activated_holo.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_list_focused_holo.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_list_longpressed_holo.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_list_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_list_selector_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_progress_bg_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_progress_primary_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_progress_secondary_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_progressbar_indeterminate_holo1.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_progressbar_indeterminate_holo2.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_progressbar_indeterminate_holo3.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_progressbar_indeterminate_holo4.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_progressbar_indeterminate_holo5.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_progressbar_indeterminate_holo6.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_progressbar_indeterminate_holo7.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_progressbar_indeterminate_holo8.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_scrubber_control_disabled_holo.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_scrubber_control_focused_holo.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_scrubber_control_normal_holo.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_scrubber_control_pressed_holo.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_scrubber_primary_holo.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_scrubber_secondary_holo.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_scrubber_track_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_spinner_default_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_spinner_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_spinner_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_spinner_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_switch_bg_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_switch_bg_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_switch_bg_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_switch_thumb_activated_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_switch_thumb_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_switch_thumb_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_switch_thumb_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_text_select_handle_left.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_text_select_handle_middle.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_text_select_handle_right.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_textfield_activated_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_textfield_default_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_textfield_disabled_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_textfield_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-mdpi/flocktheme_textfield_focused_holo_light.9.png create mode 100644 flock/src/main/res/drawable-mdpi/icon_calendar.png create mode 100644 flock/src/main/res/drawable-mdpi/icon_card.png create mode 100644 flock/src/main/res/drawable-mdpi/icon_lock.png create mode 100644 flock/src/main/res/drawable-xhdpi/alert_error_dark.png create mode 100644 flock/src/main/res/drawable-xhdpi/alert_warning_light.png create mode 100644 flock/src/main/res/drawable-xhdpi/big_flock_icon.png create mode 100644 flock/src/main/res/drawable-xhdpi/content_copy.png create mode 100644 flock/src/main/res/drawable-xhdpi/content_discard.png create mode 100644 flock/src/main/res/drawable-xhdpi/content_edit.png create mode 100644 flock/src/main/res/drawable-xhdpi/content_new.png create mode 100644 flock/src/main/res/drawable-xhdpi/flock_actionbar_icon.png create mode 100644 flock/src/main/res/drawable-xhdpi/flock_icon.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_off_disabled_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_off_disabled_holo_light.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_off_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_off_holo_light.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_off_pressed_holo_light.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_on_disabled_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_on_disabled_holo_light.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_on_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_on_holo_light.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_on_pressed_holo_light.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_default_disabled_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_default_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_default_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_default_normal_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_default_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_radio_off_disabled_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_radio_off_disabled_holo_light.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_radio_off_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_radio_off_holo_light.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_radio_off_pressed_holo_light.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_radio_on_disabled_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_radio_on_disabled_holo_light.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_radio_on_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_radio_on_holo_light.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_radio_on_pressed_holo_light.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_off_disabled_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_off_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_off_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_off_normal_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_off_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_on_disabled_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_on_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_on_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_on_normal_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_on_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_fastscroll_thumb_default_holo.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_fastscroll_thumb_pressed_holo.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_ic_navigation_drawer.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_list_activated_holo.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_list_focused_holo.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_list_longpressed_holo.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_list_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_list_selector_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_progress_bg_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_progress_primary_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_progress_secondary_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_progressbar_indeterminate_holo1.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_progressbar_indeterminate_holo2.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_progressbar_indeterminate_holo3.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_progressbar_indeterminate_holo4.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_progressbar_indeterminate_holo5.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_progressbar_indeterminate_holo6.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_progressbar_indeterminate_holo7.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_progressbar_indeterminate_holo8.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_scrubber_control_disabled_holo.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_scrubber_control_focused_holo.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_scrubber_control_normal_holo.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_scrubber_control_pressed_holo.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_scrubber_primary_holo.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_scrubber_secondary_holo.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_scrubber_track_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_spinner_default_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_spinner_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_spinner_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_spinner_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_switch_bg_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_switch_bg_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_switch_bg_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_switch_thumb_activated_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_switch_thumb_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_switch_thumb_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_switch_thumb_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_text_select_handle_left.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_text_select_handle_middle.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_text_select_handle_right.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_textfield_activated_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_textfield_default_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_textfield_disabled_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_textfield_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xhdpi/flocktheme_textfield_focused_holo_light.9.png create mode 100644 flock/src/main/res/drawable-xhdpi/happy_cloud.png create mode 100644 flock/src/main/res/drawable-xhdpi/item_focused.9.png create mode 100644 flock/src/main/res/drawable-xhdpi/item_pressed.9.png create mode 100644 flock/src/main/res/drawable-xhdpi/sad_cloud.png create mode 100644 flock/src/main/res/drawable-xhdpi/sync_in_progress.png create mode 100644 flock/src/main/res/drawable-xxhdpi/flock_actionbar_icon.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_off_disabled_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_off_disabled_holo_light.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_off_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_off_holo_light.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_off_pressed_holo_light.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_on_disabled_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_on_disabled_holo_light.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_on_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_on_holo_light.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_on_pressed_holo_light.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_default_disabled_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_default_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_default_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_default_normal_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_default_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_off_disabled_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_off_disabled_holo_light.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_off_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_off_holo_light.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_off_pressed_holo_light.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_on_disabled_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_on_disabled_holo_light.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_on_focused_holo_light.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_on_holo_light.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_on_pressed_holo_light.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_off_disabled_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_off_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_off_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_off_normal_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_off_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_on_disabled_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_on_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_on_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_on_normal_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_on_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_fastscroll_thumb_default_holo.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_fastscroll_thumb_pressed_holo.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_ic_navigation_drawer.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_list_activated_holo.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_list_focused_holo.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_list_longpressed_holo.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_list_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_list_selector_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_progress_bg_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_progress_primary_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_progress_secondary_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_progressbar_indeterminate_holo1.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_progressbar_indeterminate_holo2.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_progressbar_indeterminate_holo3.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_progressbar_indeterminate_holo4.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_progressbar_indeterminate_holo5.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_progressbar_indeterminate_holo6.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_progressbar_indeterminate_holo7.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_progressbar_indeterminate_holo8.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_scrubber_control_disabled_holo.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_scrubber_control_focused_holo.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_scrubber_control_normal_holo.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_scrubber_control_pressed_holo.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_scrubber_primary_holo.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_scrubber_secondary_holo.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_scrubber_track_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_spinner_default_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_spinner_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_spinner_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_spinner_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_switch_bg_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_switch_bg_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_switch_bg_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_switch_thumb_activated_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_switch_thumb_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_switch_thumb_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_switch_thumb_pressed_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_text_select_handle_left.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_text_select_handle_middle.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_text_select_handle_right.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_textfield_activated_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_textfield_default_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_textfield_disabled_focused_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_textfield_disabled_holo_light.9.png create mode 100755 flock/src/main/res/drawable-xxhdpi/flocktheme_textfield_focused_holo_light.9.png create mode 100644 flock/src/main/res/drawable-xxxhdpi/flock_actionbar_icon.png create mode 100644 flock/src/main/res/drawable-xxxhdpi/flock_icon.png create mode 100644 flock/src/main/res/drawable/conversation_item_received_triangle_shape_grey.xml create mode 100644 flock/src/main/res/drawable/conversation_item_received_triangle_shape_orange.xml create mode 100644 flock/src/main/res/drawable/finish_background.xml create mode 100644 flock/src/main/res/drawable/flock_gradient.xml create mode 100755 flock/src/main/res/drawable/flocktheme_activated_background_holo_light.xml create mode 100755 flock/src/main/res/drawable/flocktheme_btn_check_holo_light.xml create mode 100755 flock/src/main/res/drawable/flocktheme_btn_default_holo_light.xml create mode 100755 flock/src/main/res/drawable/flocktheme_btn_radio_holo_light.xml create mode 100755 flock/src/main/res/drawable/flocktheme_btn_toggle_holo_light.xml create mode 100755 flock/src/main/res/drawable/flocktheme_edit_text_holo_light.xml create mode 100755 flock/src/main/res/drawable/flocktheme_fastscroll_thumb_holo.xml create mode 100755 flock/src/main/res/drawable/flocktheme_item_background_holo_light.xml create mode 100755 flock/src/main/res/drawable/flocktheme_list_selector_background_transition_holo_light.xml create mode 100755 flock/src/main/res/drawable/flocktheme_list_selector_holo_light.xml create mode 100755 flock/src/main/res/drawable/flocktheme_progress_horizontal_holo_light.xml create mode 100644 flock/src/main/res/drawable/flocktheme_progress_horizontal_holo_light_green.xml create mode 100644 flock/src/main/res/drawable/flocktheme_progress_horizontal_holo_light_red.xml create mode 100644 flock/src/main/res/drawable/flocktheme_progress_horizontal_holo_light_yellow.xml create mode 100755 flock/src/main/res/drawable/flocktheme_progress_indeterminate_horizontal_holo_light.xml create mode 100755 flock/src/main/res/drawable/flocktheme_scrubber_control_selector_holo_light.xml create mode 100755 flock/src/main/res/drawable/flocktheme_scrubber_progress_horizontal_holo_light.xml create mode 100755 flock/src/main/res/drawable/flocktheme_spinner_background_holo_light.xml create mode 100755 flock/src/main/res/drawable/flocktheme_switch_inner_holo_light.xml create mode 100755 flock/src/main/res/drawable/flocktheme_switch_track_holo_light.xml create mode 100644 flock/src/main/res/drawable/rounded_thing_grey.xml create mode 100644 flock/src/main/res/drawable/rounded_thing_orange.xml create mode 100644 flock/src/main/res/drawable/selectable_item_background.xml create mode 100644 flock/src/main/res/layout-land/activity_manage_subscription.xml create mode 100644 flock/src/main/res/layout-land/activity_send_bitcoin.xml create mode 100644 flock/src/main/res/layout-land/fragment_intro.xml create mode 100644 flock/src/main/res/layout-land/fragment_select_sync_provider.xml create mode 100644 flock/src/main/res/layout-sw320dp-land/activity_send_bitcoin.xml create mode 100644 flock/src/main/res/layout-sw320dp/activity_edit_auto_renew.xml create mode 100644 flock/src/main/res/layout-sw320dp/activity_send_bitcoin.xml create mode 100644 flock/src/main/res/layout-sw600dp-land/activity_manage_subscription.xml create mode 100644 flock/src/main/res/layout-sw600dp-land/activity_send_bitcoin.xml create mode 100644 flock/src/main/res/layout-sw600dp-land/fragment_intro.xml create mode 100644 flock/src/main/res/layout-sw600dp-land/fragment_select_sync_provider.xml create mode 100644 flock/src/main/res/layout-sw600dp/activity_edit_auto_renew.xml create mode 100644 flock/src/main/res/layout-sw600dp/activity_manage_subscription.xml create mode 100644 flock/src/main/res/layout-sw600dp/activity_send_bitcoin.xml create mode 100644 flock/src/main/res/layout-sw600dp/fragment_intro.xml create mode 100644 flock/src/main/res/layout-sw600dp/fragment_select_sync_provider.xml create mode 100644 flock/src/main/res/layout/activity_edit_auto_renew.xml create mode 100644 flock/src/main/res/layout/activity_manage_subscription.xml create mode 100644 flock/src/main/res/layout/activity_send_bitcoin.xml create mode 100644 flock/src/main/res/layout/activity_with_action_button.xml create mode 100644 flock/src/main/res/layout/change_encryption_password.xml create mode 100644 flock/src/main/res/layout/correct_encryption_password.xml create mode 100644 flock/src/main/res/layout/correct_password.xml create mode 100644 flock/src/main/res/layout/dialog_addressbook_edit.xml create mode 100644 flock/src/main/res/layout/dialog_calendar_edit.xml create mode 100644 flock/src/main/res/layout/fragment_import_other_account.xml create mode 100644 flock/src/main/res/layout/fragment_import_ows_account.xml create mode 100644 flock/src/main/res/layout/fragment_intro.xml create mode 100644 flock/src/main/res/layout/fragment_list_sync_collections.xml create mode 100644 flock/src/main/res/layout/fragment_register_ows_account.xml create mode 100644 flock/src/main/res/layout/fragment_select_sync_provider.xml create mode 100644 flock/src/main/res/layout/fragment_server_tests.xml create mode 100644 flock/src/main/res/layout/fragment_simple_list.xml create mode 100644 flock/src/main/res/layout/row_account_contact_details.xml create mode 100644 flock/src/main/res/layout/row_local_calendar_details.xml create mode 100644 flock/src/main/res/layout/row_remote_addressbook_details.xml create mode 100644 flock/src/main/res/layout/row_remote_calendar_details.xml create mode 100644 flock/src/main/res/layout/setup_activity.xml create mode 100644 flock/src/main/res/layout/simple_fragment_activity.xml create mode 100644 flock/src/main/res/layout/status_header_view.xml create mode 100644 flock/src/main/res/layout/unregister_account.xml create mode 100644 flock/src/main/res/menu/addressbook_list_browse.xml create mode 100644 flock/src/main/res/menu/calendar_list_browse.xml create mode 100644 flock/src/main/res/menu/collection_list_delete.xml create mode 100644 flock/src/main/res/menu/collection_list_edit.xml create mode 100644 flock/src/main/res/menu/manage_subscription.xml create mode 100644 flock/src/main/res/values/attrs.xml create mode 100644 flock/src/main/res/values/colors.xml create mode 100755 flock/src/main/res/values/colors_flocktheme.xml create mode 100644 flock/src/main/res/values/dimens.xml create mode 100644 flock/src/main/res/values/integers.xml create mode 100644 flock/src/main/res/values/strings.xml create mode 100644 flock/src/main/res/values/styles.xml create mode 100755 flock/src/main/res/values/styles_flocktheme.xml create mode 100755 flock/src/main/res/values/themes_flocktheme.xml create mode 100644 flock/src/main/res/xml/account_authenticator.xml create mode 100644 flock/src/main/res/xml/account_preferences.xml create mode 100644 flock/src/main/res/xml/calendars_syncadapter.xml create mode 100644 flock/src/main/res/xml/contacts_structure.xml create mode 100644 flock/src/main/res/xml/contacts_syncadapter.xml create mode 100644 flock/src/main/res/xml/keys_syncadapter.xml create mode 100644 flock/src/main/res/xml/preferences.xml 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 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d34334 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +*~ +.classpath +project.properties +.project +.settings +bin/ +gen/ +.idea/ +*.iml +out +tests +lint.xml +local.properties +ant.properties +.DS_Store +build.log +build-log.xml +.gradle +build +signing.properties diff --git a/BUILDING.md b/BUILDING.md new file mode 100644 index 0000000..342b69a --- /dev/null +++ b/BUILDING.md @@ -0,0 +1,53 @@ +Building Flock +===================== + +Basics +------ + +Flock uses [Gradle](http://gradle.org) to build the project and to maintain +dependencies. + +Building Flock +------------------- + +The following steps should help you (re)build flock from the command line. + +1. Checkout the source somewhere on your filesystem with + + git clone https://github.com/WhisperSystems/Flock.git + +2. Make sure you have the [Android SDK](https://developer.android.com/sdk/index.html) installed somewhere on your system. +3. Ensure the "Android Support Repository" and "Android SDK Build-tools" are installed from the Android SDK manager. +4. Create a local.properties file at the root of your source checkout and add an sdk.dir entry to it. + + sdk.dir=\ + +5. Execute Gradle: + + ./gradlew build + +Setting up a development environment +------------------------------------ + +[Android Studio](https://developer.android.com/sdk/installing/studio.html) is the recommended development environment. + +1. Install Android Studio +2. Make sure the "Android Support Repository" is installed in the Android Studio SDK. +3. Make sure the latest "Android SDK build-tools" is installed in the Android Studio SDK. +4. Create a new Android Studio project. from the Quickstart pannel (use File > Close Project to see it), choose "Checkout from Version Control" then "git". +5. Paste the URL for the Flock project when prompted (https://github.com/rhodey/securesync.git) +6. Android studio should detect the presence of a project file and ask you wethere to open it. Click "yes". +7. Default config options should be good enough. +8. Project initialisation and build should proceed. + +Contributing code +----------------- + +Code contributions should be sent via github as pull requests, from feature branches [as explained here](https://help.github.com/articles/using-pull-requests). + +Mailing list +------------ + +Development discussion happens on the whispersystems mailing list. +[To join](https://lists.riseup.net/www/info/whispersystems) +Send emails to whispersystems@lists.riseup.net diff --git a/README.md b/README.md new file mode 100644 index 0000000..f09dfd4 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +Flock +================= + +A secure contact and calendar syncing application for Android. + +Flock is a replacement for the default contact and calendar synchronization services provided by Google. Flock provides end-to-end encryption +between multiple Android devices, putting the privacy of your contacts and calendars back under your control. To facilitate syncing between +multiple devices a "Sync Service" is required, this can be any standards compliant WebDAV server that conforms to the following RFCs: + +1. RFC 2518 - HTTP Extensions for Distributed Authoring -- WEBDAV +2. RFC 4918 - HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV) +3. RFC 6352 - CardDAV: vCard Extensions to Web Distributed Authoring and Versioning (WebDAV) +4. RFC 4791 - Calendaring Extensions to WebDAV (CalDAV) +5. RFC 1337 - TODO: A few more small specs to include... + +Building and contributing code +============================== +Instructions on how to build Flock, as well as on how to setup an IDE to modify it can be found in the "BUILDING.md" file. + +Bug tracker +----------- + +Have a bug? Please create an issue here on GitHub! + +https://github.com/rhodey/securesync/issues + +Mailing list +------------ + +Have a question? Ask on our mailing list! + +whispersystems@lists.riseup.net + +https://lists.riseup.net/www/info/whispersystems + +Cryptography Notice +------------ + +This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of encryption software. +BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. +See for more information. + +The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms. +The form and manner of this distribution makes it eligible for export under the License Exception ENC Technology Software Unrestricted (TSU) exception (see the BIS Export Administration Regulations, Section 740.13) for both object code and source code. + +License +--------------------- + +Copyright 2014 Open WhisperSystems + +Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..495c503 --- /dev/null +++ b/build.gradle @@ -0,0 +1 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. diff --git a/flock/.gitignore b/flock/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/flock/.gitignore @@ -0,0 +1 @@ +/build diff --git a/flock/build.gradle b/flock/build.gradle new file mode 100644 index 0000000..88b4930 --- /dev/null +++ b/flock/build.gradle @@ -0,0 +1,90 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:0.12.+' + classpath files('libs/gradle-witness.jar') + } +} + +apply plugin: 'com.android.application' +apply plugin: 'witness' + +repositories { + mavenCentral() + + maven { + url "https://raw.github.com/whispersystems/maven/master/stripe-btc/releases" + } + maven { + url "https://raw.github.com/whispersystems/maven/master/gson/releases/" + } +} + +android { + compileSdkVersion 19 + buildToolsVersion '19.1.0' + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 19 + } + + packagingOptions { + exclude 'META-INF/NOTICE' + exclude 'META-INF/LICENSE' + exclude 'META-INF/DEPENDENCIES' + exclude 'META-INF/NOTICE.txt' + exclude 'META-INF/LICENSE.txt' + exclude 'META-INF/DEPENDENCIES.txt' + exclude 'LICENSE.txt' + } + + lintOptions { + abortOnError false; + } +} + +dependencies { + compile group: 'com.google.guava', name: 'guava', version: '16.0' + compile group: 'com.android.support', name: 'support-v4', version: '19.0.1' + compile group: 'org.apache.jackrabbit', name: 'jackrabbit-webdav', version: '2.3.7' + compile group: 'commons-httpclient', name: 'commons-httpclient', version: '3.1' + compile group: 'javax.servlet', name: 'servlet-api', version: '2.5' + compile group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.5' + compile group: 'com.googlecode.ez-vcard', name: 'ez-vcard', version: '0.9.0' + compile group: 'org.mnode.ical4j', name: 'ical4j', version: '1.0.5.2' + compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.3.0' + compile group: 'com.google.code.gson', name: 'gson', version: '2.2.4' + compile group: 'com.stripe', name: 'stripe-java-btc', version: '1.12.0-btc-beta' + compile group: 'com.google.zxing', name: 'core', version: '3.1.0' +} + +dependencyVerification { + verify = [ + 'com.google.guava:guava:fa917f4f3f6a76375134ba89a40d3a1ce807945a91bbdbe39c31f76e03030868', + 'com.android.support:support-v4:a4268abd6370c3fd3f94d2a7f9e6e755f5ddd62450cf8bbc62ba789e1274d585', + 'org.apache.jackrabbit:jackrabbit-webdav:9a11e030921bc21de7d6dcf168571a3d2671f72c351498a0daefaf79e0edc888', + 'commons-httpclient:commons-httpclient:dbd4953d013e10e7c1cc3701a3e6ccd8c950c892f08d804fabfac21705930443', + 'javax.servlet:servlet-api:c658ea360a70faeeadb66fb3c90a702e4142a0ab7768f9ae9828678e0d9ad4dc', + 'org.slf4j:slf4j-simple:6d06eedca4768119b2c6ac5adf97e7b5ad57a03d6578d69e734fa083c957dc06', + 'com.googlecode.ez-vcard:ez-vcard:2e09cc227f8f269be5fbdc17b999b9514e239281754a0b4d4c221e29ba2d0876', + 'org.mnode.ical4j:ical4j:dedb09e8975f0703b3ae5ad60205fedda7512f91d837070be99732001a08b138', + 'com.fasterxml.jackson.core:jackson-databind:9b789c2de23ff5a1ae1fc8193ea79e34f16d74c64c51491fbe76ca277349e694', + 'com.google.code.gson:gson:c0328cd07ca9e363a5acd00c1cf4afe8cf554bd6d373834981ba05cebec687fb', + 'com.stripe:stripe-java-btc:daaabd181eb6bc4868d739e023226d344a56bf6020a7ad057dc0c26fc7d223da', + 'com.google.zxing:core:f00b32f7a1b0edc914a8f74301e8dc34f189afc4698e9c8cc54e5d46772734a5', + 'org.slf4j:jcl-over-slf4j:261e66d2b5d95ce1bc9923ab5e614f58b2568787b2b55f74dbb861b60ff798e0', + 'org.jsoup:jsoup:37e3b44fb9476a677a956fb684090b9b4a1e4d38bcfc30b499dbc83f109422c0', + 'org.freemarker:freemarker:c26923394f3f1cf0427f515ee3bb6be66d1a7f4261e6d6f0504fdec63ab85da8', + 'commons-logging:commons-logging:70903f6fc82e9908c8da9f20443f61d90f0870a312642991fe8462a0b9391784', + 'commons-codec:commons-codec:599b40b94b4a39c2550a4b5106df071aa03199b71ad5423207e2e7356aa4f8bb', + 'commons-lang:commons-lang:50f11b09f877c294d56f24463f47d28f929cf5044f648661c0f0cfbae9a2f49c', + 'backport-util-concurrent:backport-util-concurrent:f5759b7fcdfc83a525a036deedcbd32e5b536b625ebc282426f16ca137eb5902', + 'com.fasterxml.jackson.core:jackson-annotations:0c8c3811322cc84c09a93f34436fe784a1259dd5376a90aec5a73493456f757d', + 'org.slf4j:slf4j-api:fe30825245d2336c859dc38d60c0fc5f3668dbf29cd586828d2b5667ec355b91', + 'com.fasterxml.jackson.core:jackson-core:61f84f93e3f901134d7498b50119ee01074f10d59560e45ccd3e1d48cfec493b', + ] +} + diff --git a/flock/libs/gradle-witness.jar b/flock/libs/gradle-witness.jar new file mode 100644 index 0000000000000000000000000000000000000000..62a8d5c5250c0818c484fa579c976b252028477c GIT binary patch literal 21671 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 literal 0 HcmV?d00001 diff --git a/flock/src/androidTest/java/org/anhonesteffort/flock/test/sync/MockMasterCipher.java b/flock/src/androidTest/java/org/anhonesteffort/flock/test/sync/MockMasterCipher.java new file mode 100644 index 0000000..d753a2e --- /dev/null +++ b/flock/src/androidTest/java/org/anhonesteffort/flock/test/sync/MockMasterCipher.java @@ -0,0 +1,38 @@ +package org.anhonesteffort.flock.test.sync; + +import org.anhonesteffort.flock.crypto.MasterCipher; +import org.anhonesteffort.flock.util.Base64; + +import java.io.IOException; + +/** + * Programmer: rhodey + * Date: 3/21/14 + */ + +// This class is meant to assist in the testing of Hiding*Store and Hiding*Collection. +public class MockMasterCipher extends MasterCipher { + + public MockMasterCipher() { + super(null, null); + } + + @Override + public byte[] encryptAndEncode(byte[] data) { + return Base64.encodeBytes(data).getBytes(); + } + + @Override + public String encryptAndEncode(String data) { + return Base64.encodeBytes(data.getBytes()); + } + + @Override + public byte[] decodeAndDecrypt(byte[] encodedIvCiphertextAndMac) { + return Base64.decode(encodedIvCiphertextAndMac); + } + + public String decodeAndDecrypt(String data) throws IOException { + return new String(Base64.decode(data)); + } +} diff --git a/flock/src/androidTest/java/org/anhonesteffort/flock/test/sync/addressbook/HidingCardDavCollectionTest.java b/flock/src/androidTest/java/org/anhonesteffort/flock/test/sync/addressbook/HidingCardDavCollectionTest.java new file mode 100644 index 0000000..ad19f70 --- /dev/null +++ b/flock/src/androidTest/java/org/anhonesteffort/flock/test/sync/addressbook/HidingCardDavCollectionTest.java @@ -0,0 +1,84 @@ +package org.anhonesteffort.flock.test.sync.addressbook; + +import android.test.AndroidTestCase; + +import com.google.common.base.Optional; +import ezvcard.VCard; +import ezvcard.VCardVersion; +import ezvcard.property.StructuredName; +import ezvcard.property.Uid; +import org.anhonesteffort.flock.test.sync.MockMasterCipher; +import org.anhonesteffort.flock.sync.addressbook.HidingCardDavCollection; +import org.anhonesteffort.flock.sync.addressbook.HidingCardDavStore; +import org.anhonesteffort.flock.webdav.ComponentETagPair; +import org.anhonesteffort.flock.test.webdav.DavTestParams; + +import java.util.UUID; + +/** + * Programmer: rhodey + * Date: 2/25/14 + */ +public class HidingCardDavCollectionTest extends AndroidTestCase { + + private HidingCardDavCollection hidingCardDavCollection; + + @Override + protected void setUp() throws Exception { + HidingCardDavStore hidingCardDavStore = new HidingCardDavStore(new MockMasterCipher(), + DavTestParams.WEBDAV_HOST, + DavTestParams.USERNAME, + DavTestParams.PASSWORD, + Optional.absent(), + Optional.absent()); + + Optional addressbookHomeSet = hidingCardDavStore.getAddressbookHomeSet(); + String COLLECTION_PATH = addressbookHomeSet.get().concat("addressbook/"); + + hidingCardDavCollection = hidingCardDavStore.getCollection(COLLECTION_PATH).get(); + } + + public void testEditProperties() throws Exception { + final Optional ORIGINAL_DISPLAY_NAME = hidingCardDavCollection.getHiddenDisplayName(); + + final String NEW_DISPLAY_NAME = "Only 1337 people in here"; + + hidingCardDavCollection.setHiddenDisplayName(NEW_DISPLAY_NAME); + + assertEquals("Addressbook display name must be maintained.", + NEW_DISPLAY_NAME, + hidingCardDavCollection.getHiddenDisplayName().get()); + + if (ORIGINAL_DISPLAY_NAME.isPresent()) + hidingCardDavCollection.setDisplayName(ORIGINAL_DISPLAY_NAME.get()); + } + + public void testAddGetRemoveComponent() throws Exception { + final StructuredName structuredName = new StructuredName(); + structuredName.setFamily("Strangelove"); + structuredName.setGiven("idk"); + structuredName.addPrefix("Dr"); + structuredName.addSuffix(""); + + VCard putVCard = new VCard(); + putVCard.setVersion(VCardVersion.V3_0); + putVCard.setUid(new Uid(UUID.randomUUID().toString())); + putVCard.setStructuredName(structuredName); + putVCard.setFormattedName("you need this too"); + + hidingCardDavCollection.addComponent(putVCard); + + Optional> gotVCard = hidingCardDavCollection.getComponent(putVCard.getUid().getValue()); + assertTrue("Added component must be found in collection.", gotVCard.isPresent()); + + assertEquals("vCard structured name must be maintained within the collection.", + gotVCard.get().getComponent().getStructuredName().getFamily(), + putVCard.getStructuredName().getFamily()); + + hidingCardDavCollection.removeComponent(putVCard.getUid().getValue()); + + assertTrue("Removed component must not be found in collection.", + !hidingCardDavCollection.getComponent(putVCard.getUid().getValue()).isPresent()); + } + +} diff --git a/flock/src/androidTest/java/org/anhonesteffort/flock/test/sync/addressbook/HidingCardDavStoreTest.java b/flock/src/androidTest/java/org/anhonesteffort/flock/test/sync/addressbook/HidingCardDavStoreTest.java new file mode 100644 index 0000000..f8250e4 --- /dev/null +++ b/flock/src/androidTest/java/org/anhonesteffort/flock/test/sync/addressbook/HidingCardDavStoreTest.java @@ -0,0 +1,41 @@ +package org.anhonesteffort.flock.test.sync.addressbook; + +import android.test.AndroidTestCase; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.test.sync.MockMasterCipher; +import org.anhonesteffort.flock.sync.addressbook.HidingCardDavStore; +import org.anhonesteffort.flock.test.webdav.DavTestParams; +import org.anhonesteffort.flock.webdav.carddav.CardDavCollection; + +import java.net.URLEncoder; + +/** + * Programmer: rhodey + * Date: 2/25/14 + */ +public class HidingCardDavStoreTest extends AndroidTestCase { + + private HidingCardDavStore hidingCardDavStore; + + @Override + protected void setUp() throws Exception { + hidingCardDavStore = new HidingCardDavStore(new MockMasterCipher(), + DavTestParams.WEBDAV_HOST, + DavTestParams.USERNAME, + DavTestParams.PASSWORD, + Optional.absent(), + Optional.absent()); + } + + public void testGetCollections() throws Exception { + final String DEFAULT_COLLECTION_OWNER = "/principals/__uids__/" + URLEncoder.encode(DavTestParams.USERNAME) + "/"; + + CardDavCollection collection = hidingCardDavStore.getCollections().get(0); + + assertEquals("Default addressbook collection must be owned by " + DavTestParams.USERNAME, + collection.getOwnerHref().get(), + DEFAULT_COLLECTION_OWNER); + } + +} diff --git a/flock/src/androidTest/java/org/anhonesteffort/flock/test/sync/calendar/HidingCalDavCollectionTest.java b/flock/src/androidTest/java/org/anhonesteffort/flock/test/sync/calendar/HidingCalDavCollectionTest.java new file mode 100644 index 0000000..51ac134 --- /dev/null +++ b/flock/src/androidTest/java/org/anhonesteffort/flock/test/sync/calendar/HidingCalDavCollectionTest.java @@ -0,0 +1,131 @@ +package org.anhonesteffort.flock.test.sync.calendar; + +import android.test.AndroidTestCase; + +import com.google.common.base.Optional; +import net.fortuna.ical4j.model.Date; +import net.fortuna.ical4j.model.TimeZone; +import net.fortuna.ical4j.model.TimeZoneRegistry; +import net.fortuna.ical4j.model.TimeZoneRegistryFactory; +import net.fortuna.ical4j.model.component.VEvent; +import net.fortuna.ical4j.model.component.VTimeZone; +import net.fortuna.ical4j.model.property.CalScale; +import net.fortuna.ical4j.model.property.Description; +import net.fortuna.ical4j.model.property.Uid; +import net.fortuna.ical4j.model.property.Version; +import net.fortuna.ical4j.util.Calendars; +import org.anhonesteffort.flock.test.sync.MockMasterCipher; +import org.anhonesteffort.flock.sync.calendar.HidingCalDavCollection; +import org.anhonesteffort.flock.sync.calendar.HidingCalDavStore; +import org.anhonesteffort.flock.webdav.ComponentETagPair; +import org.anhonesteffort.flock.test.webdav.DavTestParams; + +import java.util.Calendar; +import java.util.UUID; + +/** + * Programmer: rhodey + * Date: 2/25/14 + */ +public class HidingCalDavCollectionTest extends AndroidTestCase { + + private HidingCalDavCollection hidingCalDavCollection; + + @Override + protected void setUp() throws Exception { + HidingCalDavStore hidingCalDavStore = new HidingCalDavStore(new MockMasterCipher(), + DavTestParams.WEBDAV_HOST, + DavTestParams.USERNAME, + DavTestParams.PASSWORD, + Optional.absent(), + Optional.absent()); + + Optional calendarHomeSet = hidingCalDavStore.getCalendarHomeSet(); + String COLLECTION_PATH = calendarHomeSet.get().concat("calendar/"); + + hidingCalDavCollection = hidingCalDavStore.getCollection(COLLECTION_PATH).get(); + } + + public void testEditTimeZone() throws Exception { + net.fortuna.ical4j.model.Calendar putCalendar = new net.fortuna.ical4j.model.Calendar(); + putCalendar.getProperties().add(Version.VERSION_2_0); + putCalendar.getProperties().add(CalScale.GREGORIAN); + + TimeZoneRegistry registry = TimeZoneRegistryFactory.getInstance().createRegistry(); + TimeZone timezone = registry.getTimeZone("America/Mexico_City"); + VTimeZone putTimeZone = timezone.getVTimeZone(); + + putCalendar.getComponents().add(putTimeZone); + hidingCalDavCollection.setTimeZone(putCalendar); + + VTimeZone gotTimeZone = (VTimeZone) hidingCalDavCollection.getTimeZone().get().getComponent(VTimeZone.VTIMEZONE); + + assertEquals("Time zone thing must be maintained.", + putTimeZone.getTimeZoneId().getValue(), + gotTimeZone.getTimeZoneId().getValue()); + } + + public void testEditProperties() throws Exception { + final Optional ORIGINAL_DISPLAY_NAME = hidingCalDavCollection.getHiddenDisplayName(); + final Optional ORIGINAL_COLOR = hidingCalDavCollection.getHiddenColor(); + + final String NEW_DISPLAY_NAME = "GOTO FAIL"; + final Integer NEW_COLOR = 0xFFFFFFFF; + + hidingCalDavCollection.setHiddenDisplayName(NEW_DISPLAY_NAME); + hidingCalDavCollection.setHiddenColor(NEW_COLOR); + + assertEquals("Display name should be maintained.", NEW_DISPLAY_NAME, hidingCalDavCollection.getHiddenDisplayName().get()); + assertEquals("Color should be maintained.", NEW_COLOR, hidingCalDavCollection.getHiddenColor().get()); + + if (ORIGINAL_DISPLAY_NAME.isPresent()) + hidingCalDavCollection.setDisplayName(ORIGINAL_DISPLAY_NAME.get()); + if (ORIGINAL_COLOR.isPresent()) + hidingCalDavCollection.setColor(ORIGINAL_COLOR.get()); + } + + public void testAddGetRemoveComponent() throws Exception { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.MONTH, Calendar.JUNE); + calendar.set(Calendar.DAY_OF_MONTH, 5); + + net.fortuna.ical4j.model.Calendar putCalendar = new net.fortuna.ical4j.model.Calendar(); + putCalendar.getProperties().add(Version.VERSION_2_0); + putCalendar.getProperties().add(CalScale.GREGORIAN); + + Date putStartDate = new Date(calendar.getTime()); + Date putEndDate = new Date(putStartDate.getTime() + (1000 * 60 * 60 * 24)); + + VEvent vEventPut = new VEvent(putStartDate, putEndDate, "Tank Man"); + vEventPut.getProperties().add(new Uid(UUID.randomUUID().toString())); + vEventPut.getProperties().add(new Description("THIS IS A LINE LONG ENOUGH TO BE SPLIT IN TWO BY THE ICAL FOLDING NONSENSE WHY DOES THIS EXIST?!?!??!?!?!?!?!??")); + putCalendar.getComponents().add(vEventPut); + + hidingCalDavCollection.addComponent(putCalendar); + + Optional> gotCalendar = hidingCalDavCollection.getComponent(Calendars.getUid(putCalendar).getValue()); + assertTrue("Added component must be found in the collection.", gotCalendar.isPresent()); + + VEvent vEventGot = (VEvent) putCalendar.getComponent(VEvent.VEVENT); + assertEquals("vEvent summary must be maintained.", + vEventGot.getSummary().getValue(), + vEventPut.getSummary().getValue()); + + assertEquals("vEvent description must be maintained.", + vEventGot.getDescription().getValue(), + vEventPut.getDescription().getValue()); + + assertEquals("VEvent start date must be maintained.", + vEventGot.getStartDate().getDate().getTime(), + putStartDate.getTime()); + + assertEquals("VEvent end date must be maintained.", + vEventGot.getEndDate().getDate().getTime(), + putEndDate.getTime()); + + hidingCalDavCollection.removeComponent(Calendars.getUid(putCalendar).getValue()); + assertTrue("Removed component must not be found in the collection.", + !hidingCalDavCollection.getComponent(Calendars.getUid(putCalendar).getValue()).isPresent()); + } + +} diff --git a/flock/src/androidTest/java/org/anhonesteffort/flock/test/sync/calendar/HidingCalDavStoreTest.java b/flock/src/androidTest/java/org/anhonesteffort/flock/test/sync/calendar/HidingCalDavStoreTest.java new file mode 100644 index 0000000..f4dcee0 --- /dev/null +++ b/flock/src/androidTest/java/org/anhonesteffort/flock/test/sync/calendar/HidingCalDavStoreTest.java @@ -0,0 +1,78 @@ +package org.anhonesteffort.flock.test.sync.calendar; + +import android.test.AndroidTestCase; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.test.sync.MockMasterCipher; +import org.anhonesteffort.flock.sync.calendar.HidingCalDavCollection; +import org.anhonesteffort.flock.sync.calendar.HidingCalDavStore; +import org.anhonesteffort.flock.test.webdav.DavTestParams; +import org.anhonesteffort.flock.webdav.caldav.CalDavCollection; + +import java.net.URLEncoder; + +/** + * Programmer: rhodey + * Date: 2/25/14 + */ +public class HidingCalDavStoreTest extends AndroidTestCase { + + private HidingCalDavStore hidingCalDavStore; + + @Override + protected void setUp() throws Exception { + hidingCalDavStore = new HidingCalDavStore(new MockMasterCipher(), + DavTestParams.WEBDAV_HOST, + DavTestParams.USERNAME, + DavTestParams.PASSWORD, + Optional.absent(), + Optional.absent()); + } + + public void testGetCollections() throws Exception { + final String DEFAULT_COLLECTION_OWNER = "/principals/__uids__/" + URLEncoder.encode(DavTestParams.USERNAME) + "/"; + + CalDavCollection collection = hidingCalDavStore.getCollections().get(0); + + assertEquals("Default calendar collection must be owned by " + DavTestParams.USERNAME, + collection.getOwnerHref().get(), + DEFAULT_COLLECTION_OWNER); + } + + public void testAddGetRemoveSimpleCollection() throws Exception { + Optional calendarHomeSet = hidingCalDavStore.getCalendarHomeSet(); + assertTrue("Calendar home set property must be found.", calendarHomeSet.isPresent()); + + final String COLLECTION_PATH = calendarHomeSet.get().concat("delete-me/"); + + hidingCalDavStore.addCollection(COLLECTION_PATH); + assertTrue("Added collection must be found in the store.", + hidingCalDavStore.getCollection(COLLECTION_PATH).isPresent()); + + hidingCalDavStore.removeCollection(COLLECTION_PATH); + assertTrue("Removed collection must not be found in the store.", + !hidingCalDavStore.getCollection(COLLECTION_PATH).isPresent()); + } + + public void testAddRemoveCollectionWithProperties() throws Exception { + final String DISPLAY_NAME = "Test Collection"; + final Integer COLOR = 0xFFFFFFFF; + + Optional calendarHomeSet = hidingCalDavStore.getCalendarHomeSet(); + assertTrue("Calendar home set property must be found.", calendarHomeSet.isPresent()); + + final String COLLECTION_PATH = calendarHomeSet.get().concat("delete-me/"); + + hidingCalDavStore.addCollection(COLLECTION_PATH, DISPLAY_NAME, COLOR); + Optional collection = hidingCalDavStore.getCollection(COLLECTION_PATH); + + assertTrue( "Added collection must be found in the store.", collection.isPresent()); + assertEquals("Added collection display name must be maintained.", collection.get().getHiddenDisplayName().get(), DISPLAY_NAME); + assertEquals("Added collection color must be maintained.", collection.get().getHiddenColor().get(), COLOR); + + hidingCalDavStore.removeCollection(COLLECTION_PATH); + assertTrue("Removed collection must not be found in the store.", + !hidingCalDavStore.getCollection(COLLECTION_PATH).isPresent()); + } + +} diff --git a/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/DavCollectionTest.java b/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/DavCollectionTest.java new file mode 100644 index 0000000..3c65df1 --- /dev/null +++ b/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/DavCollectionTest.java @@ -0,0 +1,46 @@ +package org.anhonesteffort.flock.test.webdav; + +import android.test.AndroidTestCase; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.webdav.caldav.CalDavCollection; +import org.anhonesteffort.flock.webdav.caldav.CalDavStore; + +/** + * Programmer: rhodey + * Date: 2/4/14 + */ +public class DavCollectionTest extends AndroidTestCase { + + private CalDavCollection davCollection; + + @Override + protected void setUp() throws Exception { + CalDavStore calDavStore = new CalDavStore(DavTestParams.WEBDAV_HOST, + DavTestParams.USERNAME, + DavTestParams.PASSWORD, + Optional.absent(), + Optional.absent()); + + Optional calendarHomeSet = calDavStore.getCalendarHomeSet(); + String COLLECTION_PATH = calendarHomeSet.get().concat("calendar/"); + + davCollection = calDavStore.getCollection(COLLECTION_PATH).get(); + } + + public void testEditProperties() throws Exception { + final String DISPLAY_NAME = "calendar"; + + davCollection.setDisplayName(DISPLAY_NAME); + + assertEquals("Collection must persist property changes.", + DISPLAY_NAME, + davCollection.getDisplayName().get()); + } + + public void testCTag() throws Exception { + Optional cTag = davCollection.getCTag(); + assertTrue("All dav collections should have a CTag.", cTag.isPresent()); + } + +} diff --git a/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/DavStoreTest.java b/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/DavStoreTest.java new file mode 100644 index 0000000..979ae9c --- /dev/null +++ b/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/DavStoreTest.java @@ -0,0 +1,37 @@ +package org.anhonesteffort.flock.test.webdav; + +import android.test.AndroidTestCase; +import android.util.Log; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.webdav.caldav.CalDavStore; + +import java.util.List; + + +/** + * Programmer: rhodey + * Date: 2/4/14 + */ +public class DavStoreTest extends AndroidTestCase { + + private static final String TAG = "DavStoreTest"; + + private CalDavStore davStore; + + @Override + protected void setUp() throws Exception { + davStore = new CalDavStore(DavTestParams.WEBDAV_HOST, + DavTestParams.USERNAME, + DavTestParams.PASSWORD, + Optional.absent(), + Optional.absent()); + } + + public void testDavOptions() throws Exception { + List davOptions = davStore.getDavOptions(); + assertTrue("DAVOptions should be something.", davOptions.size() > 0); + Log.d(TAG, "DAV Options: " + davOptions); + } + +} diff --git a/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/DavTestParams.java b/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/DavTestParams.java new file mode 100644 index 0000000..9dd3089 --- /dev/null +++ b/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/DavTestParams.java @@ -0,0 +1,13 @@ +package org.anhonesteffort.flock.test.webdav; + +/** + * Programmer: rhodey + * Date: 2/25/14 + */ +public class DavTestParams { + + public static final String WEBDAV_HOST = "http://192.168.1.105:8008"; + public static final String USERNAME = "testn@flock.sync".toUpperCase(); + public static final String PASSWORD = "test"; + +} diff --git a/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/caldav/CalDavCollectionTest.java b/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/caldav/CalDavCollectionTest.java new file mode 100644 index 0000000..c94640c --- /dev/null +++ b/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/caldav/CalDavCollectionTest.java @@ -0,0 +1,145 @@ +package org.anhonesteffort.flock.test.webdav.caldav; + +import android.test.AndroidTestCase; + +import com.google.common.base.Optional; +import net.fortuna.ical4j.model.Date; +import net.fortuna.ical4j.model.TimeZone; +import net.fortuna.ical4j.model.TimeZoneRegistry; +import net.fortuna.ical4j.model.TimeZoneRegistryFactory; +import net.fortuna.ical4j.model.component.VEvent; +import net.fortuna.ical4j.model.component.VTimeZone; +import net.fortuna.ical4j.model.property.CalScale; +import net.fortuna.ical4j.model.property.Description; +import net.fortuna.ical4j.model.property.Uid; +import net.fortuna.ical4j.model.property.Version; +import net.fortuna.ical4j.util.Calendars; +import org.anhonesteffort.flock.webdav.ComponentETagPair; +import org.anhonesteffort.flock.test.webdav.DavTestParams; +import org.anhonesteffort.flock.webdav.caldav.CalDavCollection; +import org.anhonesteffort.flock.webdav.caldav.CalDavStore; + +import java.util.Calendar; +import java.util.UUID; + +/** + * Programmer: rhodey + * Date: 2/4/14 + */ +public class CalDavCollectionTest extends AndroidTestCase { + + private CalDavCollection calDavCollection; + + @Override + protected void setUp() throws Exception { + CalDavStore calDavStore = new CalDavStore(DavTestParams.WEBDAV_HOST, + DavTestParams.USERNAME, + DavTestParams.PASSWORD, + Optional.absent(), + Optional.absent()); + + Optional calendarHomeSet = calDavStore.getCalendarHomeSet(); + String COLLECTION_PATH = calendarHomeSet.get().concat("calendar/"); + + calDavCollection = calDavStore.getCollection(COLLECTION_PATH).get(); + } + + public void testEditProperties() throws Exception { + final Optional ORIGINAL_DISPLAY_NAME = calDavCollection.getDisplayName(); + final Optional ORIGINAL_DESCRIPTION = calDavCollection.getDescription(); + final Optional ORIGINAL_COLOR = calDavCollection.getColor(); + + final String NEW_DISPLAY_NAME = "GOTO FAIL"; + final String NEW_DESCRIPTION = "TYPO-- I SWEAR!"; + final Integer NEW_COLOR = 0xFF; + + calDavCollection.setDisplayName(NEW_DISPLAY_NAME); + calDavCollection.setDescription(NEW_DESCRIPTION); + calDavCollection.setColor(NEW_COLOR); + + assertEquals("Display name should be maintained.", + NEW_DISPLAY_NAME, + calDavCollection.getDisplayName().get()); + assertEquals("Description should be maintained.", + NEW_DESCRIPTION, + calDavCollection.getDescription().get()); + assertEquals("Color should be maintained.", + NEW_COLOR, + calDavCollection.getColor().get()); + + if (ORIGINAL_DISPLAY_NAME.isPresent()) + calDavCollection.setDisplayName(ORIGINAL_DISPLAY_NAME.get()); + if (ORIGINAL_DESCRIPTION.isPresent()) + calDavCollection.setDescription(ORIGINAL_DESCRIPTION.get()); + if (ORIGINAL_COLOR.isPresent()) + calDavCollection.setColor(ORIGINAL_COLOR.get()); + } + + public void testEditTimeZone() throws Exception { + net.fortuna.ical4j.model.Calendar putCalendar = new net.fortuna.ical4j.model.Calendar(); + putCalendar.getProperties().add(Version.VERSION_2_0); + putCalendar.getProperties().add(CalScale.GREGORIAN); + + TimeZoneRegistry registry = TimeZoneRegistryFactory.getInstance().createRegistry(); + TimeZone timezone = registry.getTimeZone("America/Mexico_City"); + VTimeZone putTimeZone = timezone.getVTimeZone(); + + putCalendar.getComponents().add(putTimeZone); + calDavCollection.setTimeZone(putCalendar); + + VTimeZone gotTimeZone = (VTimeZone) calDavCollection.getTimeZone().get().getComponent(VTimeZone.VTIMEZONE); + + assertEquals("Time zone thing must be maintained.", + putTimeZone.getTimeZoneId().getValue(), + gotTimeZone.getTimeZoneId().getValue()); + } + + public void testAddGetRemoveComponent() throws Exception { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.MONTH, Calendar.JUNE); + calendar.set(Calendar.DAY_OF_MONTH, 5); + + net.fortuna.ical4j.model.Calendar putCalendar = new net.fortuna.ical4j.model.Calendar(); + putCalendar.getProperties().add(Version.VERSION_2_0); + putCalendar.getProperties().add(CalScale.GREGORIAN); + + Date putStartDate = new Date(calendar.getTime()); + Date putEndDate = new Date(putStartDate.getTime() + (1000 * 60 * 60 * 24)); + + VEvent vEventPut = new VEvent(putStartDate, putEndDate, "Tank Man"); + vEventPut.getProperties().add(new Uid(UUID.randomUUID().toString())); + vEventPut.getProperties().add(new Description("THIS IS A LINE LONG ENOUGH TO BE SPLIT IN TWO BY THE ICAL FOLDING NONSENSE WHY DOES THIS EXIST?!?!??!?!?!?!?!??")); + putCalendar.getComponents().add(vEventPut); + + calDavCollection.addComponent(putCalendar); + + Optional> gotCalendar = + calDavCollection.getComponent(Calendars.getUid(putCalendar).getValue()); + assertTrue("Added component must be found in the collection.", + gotCalendar.isPresent()); + + VEvent vEventGot = (VEvent) gotCalendar.get().getComponent().getComponent(VEvent.VEVENT); + assertEquals("VEvent summary must be maintained.", + vEventGot.getSummary().getValue(), + vEventPut.getSummary().getValue()); + + assertEquals("vEvent description must be maintained.", + vEventGot.getDescription().getValue(), + vEventPut.getDescription().getValue()); + + assertEquals("VEvent start date must be maintained.", + vEventGot.getStartDate().getDate().getTime(), + putStartDate.getTime()); + + assertEquals("VEvent end date must be maintained.", + vEventGot.getEndDate().getDate().getTime(), + putEndDate.getTime()); + + assertTrue("VEvent should have an ETag", calDavCollection.getComponentETags().size() > 0); + + calDavCollection.removeComponent(Calendars.getUid(putCalendar).getValue()); + assertTrue("Removed component must not be found in the collection.", + !calDavCollection.getComponent(Calendars.getUid(putCalendar).getValue()).isPresent()); + } + +} diff --git a/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/caldav/CalDavStoreTest.java b/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/caldav/CalDavStoreTest.java new file mode 100644 index 0000000..d1369a7 --- /dev/null +++ b/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/caldav/CalDavStoreTest.java @@ -0,0 +1,86 @@ +package org.anhonesteffort.flock.test.webdav.caldav; + +import android.test.AndroidTestCase; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.test.webdav.DavTestParams; +import org.anhonesteffort.flock.webdav.caldav.CalDavCollection; +import org.anhonesteffort.flock.webdav.caldav.CalDavStore; + +import java.net.URLEncoder; + +/** + * Programmer: rhodey + * Date: 2/4/14 + */ +public class CalDavStoreTest extends AndroidTestCase { + + private CalDavStore calDavStore; + + @Override + protected void setUp() throws Exception { + calDavStore = new CalDavStore(DavTestParams.WEBDAV_HOST, + DavTestParams.USERNAME, + DavTestParams.PASSWORD, + Optional.absent(), + Optional.absent()); + } + + public void testDavCurrentUserPrincipal() throws Exception { + Optional currentUserPrincipal = calDavStore.getCurrentUserPrincipal(); + assertTrue("DAV:current-user-principal should be something.", currentUserPrincipal.isPresent()); + } + + public void testGetCollections() throws Exception { + final String DEFAULT_COLLECTION_OWNER = "/principals/__uids__/" + URLEncoder.encode(DavTestParams.USERNAME) + "/"; + + CalDavCollection collection = calDavStore.getCollections().get(0); + + assertEquals("Default calendar collection must be owned by " + DavTestParams.USERNAME, + collection.getOwnerHref().get(), + DEFAULT_COLLECTION_OWNER); + } + + public void testAddGetRemoveSimpleCollection() throws Exception { + Optional calendarHomeSet = calDavStore.getCalendarHomeSet(); + assertTrue("Calendar home set property must be found.", calendarHomeSet.isPresent()); + + final String COLLECTION_PATH = calendarHomeSet.get().concat("test-collection/"); + + calDavStore.addCollection(COLLECTION_PATH); + assertTrue("Added collection must be found in the store.", + calDavStore.getCollection(COLLECTION_PATH).isPresent()); + + calDavStore.removeCollection(COLLECTION_PATH); + assertTrue("Removed collection must not be found in the store.", + !calDavStore.getCollection(COLLECTION_PATH).isPresent()); + } + + public void testAddRemoveCollectionWithProperties() throws Exception { + final String DISPLAY_NAME = "Test Collection"; + final String DESCRIPTION = "This is a test collection."; + final Integer COLOR = 0xFF; + + Optional calendarHomeSet = calDavStore.getCalendarHomeSet(); + assertTrue("Calendar home set property must be found.", calendarHomeSet.isPresent()); + + final String COLLECTION_PATH = calendarHomeSet.get().concat("test-collection/"); + + calDavStore.addCollection(COLLECTION_PATH, DISPLAY_NAME, DESCRIPTION, COLOR); + Optional collection = calDavStore.getCollection(COLLECTION_PATH); + + assertTrue("Added collection must be found in the store.", + collection.isPresent()); + assertEquals("Added collection display name must be maintained.", + collection.get().getDisplayName().get(), DISPLAY_NAME); + assertEquals("Added collection description must be maintained.", + collection.get().getDescription().get(), DESCRIPTION); + assertEquals("Added collection color must be maintained.", + collection.get().getColor().get(), COLOR); + + calDavStore.removeCollection(COLLECTION_PATH); + assertTrue("Removed collection must not be found in the store.", + !calDavStore.getCollection(COLLECTION_PATH).isPresent()); + } + +} diff --git a/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/carddav/CardDavCollectionTest.java b/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/carddav/CardDavCollectionTest.java new file mode 100644 index 0000000..c2ade22 --- /dev/null +++ b/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/carddav/CardDavCollectionTest.java @@ -0,0 +1,90 @@ +package org.anhonesteffort.flock.test.webdav.carddav; + +import android.test.AndroidTestCase; + +import com.google.common.base.Optional; +import ezvcard.VCard; +import ezvcard.VCardVersion; +import ezvcard.property.StructuredName; +import ezvcard.property.Uid; +import org.anhonesteffort.flock.test.webdav.DavTestParams; +import org.anhonesteffort.flock.webdav.ComponentETagPair; +import org.anhonesteffort.flock.webdav.carddav.CardDavCollection; +import org.anhonesteffort.flock.webdav.carddav.CardDavStore; + +import java.util.UUID; + +/** + * Programmer: rhodey + * Date: 2/4/14 + */ +public class CardDavCollectionTest extends AndroidTestCase { + + private CardDavCollection cardDavCollection; + + @Override + protected void setUp() throws Exception { + CardDavStore cardDavStore = new CardDavStore(DavTestParams.WEBDAV_HOST, + DavTestParams.USERNAME, + DavTestParams.PASSWORD, + Optional.absent(), + Optional.absent()); + + Optional addressbookHomeSet = cardDavStore.getAddressbookHomeSet(); + String COLLECTION_PATH = addressbookHomeSet.get().concat("addressbook/"); + + cardDavCollection = cardDavStore.getCollection(COLLECTION_PATH).get(); + } + + public void testEditProperties() throws Exception { + final Optional ORIGINAL_DISPLAY_NAME = cardDavCollection.getDisplayName(); + final Optional ORIGINAL_DESCRIPTION = cardDavCollection.getDescription(); + + final String NEW_DISPLAY_NAME = "Only 1337 people in here"; + final String NEW_DESCRIPTION = "CardDAV things insist that addressbook is one word."; + + cardDavCollection.setDisplayName(NEW_DISPLAY_NAME); + cardDavCollection.setDescription(NEW_DESCRIPTION); + + assertEquals("Addressbook display name must be maintained.", + NEW_DISPLAY_NAME, + cardDavCollection.getDisplayName().get()); + assertEquals("Addressbook description must be maintained.", + NEW_DESCRIPTION, + cardDavCollection.getDescription().get()); + + if (ORIGINAL_DISPLAY_NAME.isPresent()) + cardDavCollection.setDisplayName(ORIGINAL_DISPLAY_NAME.get()); + if (ORIGINAL_DESCRIPTION.isPresent()) + cardDavCollection.setDescription(ORIGINAL_DESCRIPTION.get()); + } + + public void testAddGetRemoveComponent() throws Exception { + final StructuredName structuredName = new StructuredName(); + structuredName.setFamily("Strangelove"); + structuredName.setGiven("?"); + structuredName.addPrefix("Dr"); + structuredName.addSuffix(""); + + VCard putVCard = new VCard(); + putVCard.setVersion(VCardVersion.V3_0); + putVCard.setUid(new Uid(UUID.randomUUID().toString())); + putVCard.setStructuredName(structuredName); + putVCard.setFormattedName("you need this too"); + + cardDavCollection.addComponent(putVCard); + + Optional> gotVCard = cardDavCollection.getComponent(putVCard.getUid().getValue()); + assertTrue("Added component must be found in collection.", gotVCard.isPresent()); + + assertEquals("vCard structured name must be maintained within the collection.", + gotVCard.get().getComponent().getStructuredName().getFamily(), + structuredName.getFamily()); + + cardDavCollection.removeComponent(putVCard.getUid().getValue()); + + assertTrue("Removed component must not be found in collection.", + !cardDavCollection.getComponent(putVCard.getUid().getValue()).isPresent()); + } + +} diff --git a/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/carddav/CardDavStoreTest.java b/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/carddav/CardDavStoreTest.java new file mode 100644 index 0000000..58ffd3a --- /dev/null +++ b/flock/src/androidTest/java/org/anhonesteffort/flock/test/webdav/carddav/CardDavStoreTest.java @@ -0,0 +1,60 @@ +package org.anhonesteffort.flock.test.webdav.carddav; + +import android.test.AndroidTestCase; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.test.webdav.DavTestParams; +import org.anhonesteffort.flock.webdav.carddav.CardDavCollection; +import org.anhonesteffort.flock.webdav.carddav.CardDavStore; + +import java.net.URLEncoder; + +/** + * Programmer: rhodey + * Date: 2/4/14 + */ +public class CardDavStoreTest extends AndroidTestCase { + + private CardDavStore cardDavStore; + + @Override + protected void setUp() throws Exception { + cardDavStore = new CardDavStore(DavTestParams.WEBDAV_HOST, + DavTestParams.USERNAME, + DavTestParams.PASSWORD, + Optional.absent(), + Optional.absent()); + } + + public void testDavCurrentUserPrincipal() throws Exception { + Optional currentUserPrincipal = cardDavStore.getCurrentUserPrincipal(); + assertTrue("DAV:current-user-principal should be something.", currentUserPrincipal.isPresent()); + } + + public void testGetCollections() throws Exception { + final String DEFAULT_COLLECTION_OWNER = "/principals/__uids__/" + URLEncoder.encode(DavTestParams.USERNAME) + "/"; + + CardDavCollection collection = cardDavStore.getCollections().get(0); + + assertEquals("Default addressbook collection must be owned by " + DavTestParams.USERNAME, + collection.getOwnerHref().get(), + DEFAULT_COLLECTION_OWNER); + } + + // TODO: implement address book creation in Darwin Calendar Server so this can be tested. + /* + @Test + public void addGetRemoveSimpleCollection() throws Exception { + Optional addressbookHomeSet = cardDavStore.getAddressbookHomeSet(); + assertTrue("Address book home set property must be found.", addressbookHomeSet.isPresent()); + + final String COLLECTION_PATH = addressbookHomeSet.get().concat("test-collection/"); + + cardDavStore.addCollection(COLLECTION_PATH); + assertTrue("Added collection must be found in the store.", cardDavStore.getCollection(COLLECTION_PATH).isPresent()); + + cardDavStore.removeCollection(COLLECTION_PATH); + assertTrue("Removed collection must not be found in the store.", !cardDavStore.getCollection(COLLECTION_PATH).isPresent()); + } */ + +} diff --git a/flock/src/main/AndroidManifest.xml b/flock/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e3afcbc --- /dev/null +++ b/flock/src/main/AndroidManifest.xml @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flock/src/main/artwork/sync_in_progress.png b/flock/src/main/artwork/sync_in_progress.png new file mode 100644 index 0000000000000000000000000000000000000000..cfd59ad279751e6dc0c18b7ef2f6c3d0b995f91b GIT binary patch literal 27919 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 VNfKs;96$~ll%~E$*)5x}{{hnwS`7dI literal 0 HcmV?d00001 diff --git a/flock/src/main/assets/flock.store b/flock/src/main/assets/flock.store new file mode 100644 index 0000000000000000000000000000000000000000..47be3188ffeed909b1492cf334c2d4ee45280478 GIT binary patch literal 1892 zcmdT^`#aMM82^5^F`LX~qb9ddE_3;ACMmMyR^rKFLWRY$!scwQ#g5IfJu#kQI7EdM zsYp(U$a1Za<&l)AoFj7SC)RD$@(4GvND;B#Rar zMq>sOv5@jQBZox?(ZWuw8JQ<&M0p6cPRe3oR8OKBgj)x2tioTpIxP0F6OkN9Yoh{@ z46W@7k;xE=?6*;YOoXgS*ZTVFFw@C z$+1VgIHIO18t`@_1r)lzELNT^v<|vcKil{GRf4AOfL`Bnrp@xI=qY!_Z1RkCRjRi< z^jucFKmWQ&y11y{+V9`uYq#?85T8*~h8SrWzJjwn!MqhVnqz*@vj*Aj)-+jCcLh{NZp3`s?TRPruXx4_+FVK#{9pib<^c zC0#GMy!Uvb^vb9UGmrdf{$Se6)N4gu^u(@)WpqWfM&**12@NUN5;bz?;)`xPAzBbW z-R2=$*mOYWL%^_qcXif*m`#(W7q+O{cy5peXdc~(AIncq=LA(~zlDien7PSW6q{V> z@VRu5dwN3dwQe0@cdLbP{!t9)XQzkuz05$#Vt{ke2jL4C90b5(KBx)tK|Hi!ts0tG zgbV}+!Bt3e9bph^2wX_HFzdZ#Bl0$ra*_uj_gU-fM{WD)E0kg}l3$ zc{Sf@clh7@acdOz65tX&*6M>$=}jo*91(m&&-`v~*tW zupBon0PAxjZ4K_{!YxYt8I-J39u5wYr>9a@}ksvc!CrIN8W>sw!Vlq@R?;_uoSJrjFAd;F(uWHz(C*stkfLyh^I-)x>0H zi~uua;Heep{$*4FN8Pg+}xPseeS(WTr64J@+m( z?J;HFp7y+z&85eZv~8kM>a`>yN=5j2&SVMQ8U`CMp#&h`wrHp>>#Mi%Zd-Bj_T8~7 zrQt&!N(K1(^HgqA>+SIjWc3n8Sivrp1a9H)>9n#jjJ7*FLooI$-W})kE`E;UE@}vZ z*C>f}L~b1zN378o^h zrgk%7BC7M1-{FC|rMB=3IivMLlq(YL>=Qv!ue~v4(DEj`eP>2zv&!|pB#X>>Tz5}= zd(MnxW`Cs@SN7yNv2ERXLT2v>X0S~#{Tn&4X0d-L>QLvC215DF539{?4^58<#2R!; zW+_#@ 0) { + drawPointerArrow(canvas); + } + + } + + private void drawPointerArrow(Canvas canvas) { + + int centerX = getWidth() / 2; + int centerY = getHeight() / 2; + + double tipAngle = (colorHSV[2] - 0.5f) * Math.PI; + double leftAngle = tipAngle + Math.PI / 96; + double rightAngle = tipAngle - Math.PI / 96; + + double tipAngleX = Math.cos(tipAngle) * outerWheelRadius; + double tipAngleY = Math.sin(tipAngle) * outerWheelRadius; + double leftAngleX = Math.cos(leftAngle) * (outerWheelRadius + arrowPointerSize); + double leftAngleY = Math.sin(leftAngle) * (outerWheelRadius + arrowPointerSize); + double rightAngleX = Math.cos(rightAngle) * (outerWheelRadius + arrowPointerSize); + double rightAngleY = Math.sin(rightAngle) * (outerWheelRadius + arrowPointerSize); + + arrowPointerPath.reset(); + arrowPointerPath.moveTo((float) tipAngleX + centerX, (float) tipAngleY + centerY); + arrowPointerPath.lineTo((float) leftAngleX + centerX, (float) leftAngleY + centerY); + arrowPointerPath.lineTo((float) rightAngleX + centerX, (float) rightAngleY + centerY); + arrowPointerPath.lineTo((float) tipAngleX + centerX, (float) tipAngleY + centerY); + + valuePointerArrowPaint.setColor(Color.HSVToColor(colorHSV)); + valuePointerArrowPaint.setStyle(Style.FILL); + canvas.drawPath(arrowPointerPath, valuePointerArrowPaint); + + valuePointerArrowPaint.setStyle(Style.STROKE); + valuePointerArrowPaint.setStrokeJoin(Join.ROUND); + valuePointerArrowPaint.setColor(Color.BLACK); + canvas.drawPath(arrowPointerPath, valuePointerArrowPaint); + + } + + @Override + protected void onSizeChanged(int width, int height, int oldw, int oldh) { + + int centerX = width / 2; + int centerY = height / 2; + + innerPadding = (int) (paramInnerPadding * width / 100); + outerPadding = (int) (paramOuterPadding * width / 100); + arrowPointerSize = (int) (paramArrowPointerSize * width / 100); + valueSliderWidth = (int) (paramValueSliderWidth * width / 100); + + outerWheelRadius = width / 2 - outerPadding - arrowPointerSize; + innerWheelRadius = outerWheelRadius - valueSliderWidth; + colorWheelRadius = innerWheelRadius - innerPadding; + + outerWheelRect.set(centerX - outerWheelRadius, centerY - outerWheelRadius, centerX + outerWheelRadius, centerY + outerWheelRadius); + innerWheelRect.set(centerX - innerWheelRadius, centerY - innerWheelRadius, centerX + innerWheelRadius, centerY + innerWheelRadius); + + colorWheelBitmap = createColorWheelBitmap(colorWheelRadius * 2, colorWheelRadius * 2); + + gradientRotationMatrix = new Matrix(); + gradientRotationMatrix.preRotate(270, width / 2, height / 2); + + colorViewPath.arcTo(outerWheelRect, 270, -180); + colorViewPath.arcTo(innerWheelRect, 90, 180); + + valueSliderPath.arcTo(outerWheelRect, 270, 180); + valueSliderPath.arcTo(innerWheelRect, 90, -180); + + } + + private Bitmap createColorWheelBitmap(int width, int height) { + + Bitmap bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888); + + int colorCount = 12; + int colorAngleStep = 360 / 12; + int colors[] = new int[colorCount + 1]; + float hsv[] = new float[] { 0f, 1f, 1f }; + for (int i = 0; i < colors.length; i++) { + hsv[0] = (i * colorAngleStep + 180) % 360; + colors[i] = Color.HSVToColor(hsv); + } + colors[colorCount] = colors[0]; + + SweepGradient sweepGradient = new SweepGradient(width / 2, height / 2, colors, null); + RadialGradient radialGradient = new RadialGradient(width / 2, height / 2, colorWheelRadius, 0xFFFFFFFF, 0x00FFFFFF, TileMode.CLAMP); + ComposeShader composeShader = new ComposeShader(sweepGradient, radialGradient, PorterDuff.Mode.SRC_OVER); + + colorWheelPaint.setShader(composeShader); + + Canvas canvas = new Canvas(bitmap); + canvas.drawCircle(width / 2, height / 2, colorWheelRadius, colorWheelPaint); + + return bitmap; + + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + int action = event.getAction(); + switch (action) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + + int x = (int) event.getX(); + int y = (int) event.getY(); + int cx = x - getWidth() / 2; + int cy = y - getHeight() / 2; + double d = Math.sqrt(cx * cx + cy * cy); + + if (d <= colorWheelRadius) { + + colorHSV[0] = (float) (Math.toDegrees(Math.atan2(cy, cx)) + 180f); + colorHSV[1] = Math.max(0f, Math.min(1f, (float) (d / colorWheelRadius))); + + invalidate(); + + } else if (x >= getWidth() / 2 && d >= innerWheelRadius) { + + colorHSV[2] = (float) Math.max(0, Math.min(1, Math.atan2(cy, cx) / Math.PI + 0.5f)); + + invalidate(); + } + + return true; + } + return super.onTouchEvent(event); + } + + public void setColor(int color) { + Color.colorToHSV(color, colorHSV); + } + + public int getColor() { + return Color.HSVToColor(colorHSV); + } + + @Override + protected Parcelable onSaveInstanceState() { + Bundle state = new Bundle(); + state.putFloatArray("color", colorHSV); + state.putParcelable("super", super.onSaveInstanceState()); + return state; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state instanceof Bundle) { + Bundle bundle = (Bundle) state; + colorHSV = bundle.getFloatArray("color"); + super.onRestoreInstanceState(bundle.getParcelable("super")); + } else { + super.onRestoreInstanceState(state); + } + } + +} diff --git a/flock/src/main/java/com/chiralcode/colorpicker/ColorPickerPreference.java b/flock/src/main/java/com/chiralcode/colorpicker/ColorPickerPreference.java new file mode 100644 index 0000000..48ebd34 --- /dev/null +++ b/flock/src/main/java/com/chiralcode/colorpicker/ColorPickerPreference.java @@ -0,0 +1,73 @@ +package com.chiralcode.colorpicker; + + +import android.app.AlertDialog.Builder; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.preference.DialogPreference; +import android.util.AttributeSet; +import android.view.View; +import android.widget.RelativeLayout; +import android.widget.RelativeLayout.LayoutParams; + +public class ColorPickerPreference extends DialogPreference { + + public static final int DEFAULT_COLOR = Color.WHITE; + + private int selectedColor; + private ColorPicker colorPickerView; + + public ColorPickerPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected View onCreateDialogView() { + + RelativeLayout relativeLayout = new RelativeLayout(getContext()); + LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT); + + colorPickerView = new ColorPicker(getContext()); + colorPickerView.setId(1); + + relativeLayout.addView(colorPickerView, layoutParams); + + return relativeLayout; + + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + colorPickerView.setColor(selectedColor); + } + + @Override + protected void onPrepareDialogBuilder(Builder builder) { + super.onPrepareDialogBuilder(builder); + builder.setTitle(null); // remove dialog title to get more space for color picker + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + if (positiveResult && shouldPersist()) { + if (callChangeListener(colorPickerView.getColor())) { + selectedColor = colorPickerView.getColor(); + persistInt(selectedColor); + } + } + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + selectedColor = restoreValue ? getPersistedInt(DEFAULT_COLOR) : (Integer) defaultValue; + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getInt(index, DEFAULT_COLOR); + } + +} diff --git a/flock/src/main/java/com/example/android/wizardpager/wizard/ui/StepPagerStrip.java b/flock/src/main/java/com/example/android/wizardpager/wizard/ui/StepPagerStrip.java new file mode 100644 index 0000000..ce43466 --- /dev/null +++ b/flock/src/main/java/com/example/android/wizardpager/wizard/ui/StepPagerStrip.java @@ -0,0 +1,275 @@ +/* + * * + * Copyright (C) 2014 Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package com.example.android.wizardpager.wizard.ui; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; + +import org.anhonesteffort.flock.R; + +public class StepPagerStrip extends View { + private static final int[] ATTRS = new int[]{ + android.R.attr.gravity + }; + private int mPageCount; + private int mCurrentPage; + + private int mGravity = Gravity.LEFT | Gravity.TOP; + private float mTabWidth; + private float mTabHeight; + private float mTabSpacing; + + private Paint mPrevTabPaint; + private Paint mSelectedTabPaint; + private Paint mSelectedLastTabPaint; + private Paint mNextTabPaint; + + private RectF mTempRectF = new RectF(); + + //private Scroller mScroller; + + private OnPageSelectedListener mOnPageSelectedListener; + + public StepPagerStrip(Context context) { + this(context, null, 0); + } + + public StepPagerStrip(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public StepPagerStrip(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + final TypedArray a = context.obtainStyledAttributes(attrs, ATTRS); + mGravity = a.getInteger(0, mGravity); + a.recycle(); + + final Resources res = getResources(); + mTabWidth = res.getDimensionPixelSize(R.dimen.step_pager_tab_width); + mTabHeight = res.getDimensionPixelSize(R.dimen.step_pager_tab_height); + mTabSpacing = res.getDimensionPixelSize(R.dimen.step_pager_tab_spacing); + + mPrevTabPaint = new Paint(); + mPrevTabPaint.setColor(res.getColor(R.color.step_pager_previous_tab_color)); + + mSelectedTabPaint = new Paint(); + mSelectedTabPaint.setColor(res.getColor(R.color.step_pager_selected_tab_color)); + + mSelectedLastTabPaint = new Paint(); + mSelectedLastTabPaint.setColor(res.getColor(R.color.step_pager_selected_last_tab_color)); + + mNextTabPaint = new Paint(); + mNextTabPaint.setColor(res.getColor(R.color.step_pager_next_tab_color)); + } + + public void setOnPageSelectedListener(OnPageSelectedListener onPageSelectedListener) { + mOnPageSelectedListener = onPageSelectedListener; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (mPageCount == 0) { + return; + } + + float totalWidth = mPageCount * (mTabWidth + mTabSpacing) - mTabSpacing; + float totalLeft; + boolean fillHorizontal = false; + + switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.CENTER_HORIZONTAL: + totalLeft = (getWidth() - totalWidth) / 2; + break; + case Gravity.RIGHT: + totalLeft = getWidth() - getPaddingRight() - totalWidth; + break; + case Gravity.FILL_HORIZONTAL: + totalLeft = getPaddingLeft(); + fillHorizontal = true; + break; + default: + totalLeft = getPaddingLeft(); + } + + switch (mGravity & Gravity.VERTICAL_GRAVITY_MASK) { + case Gravity.CENTER_VERTICAL: + mTempRectF.top = (int) (getHeight() - mTabHeight) / 2; + break; + case Gravity.BOTTOM: + mTempRectF.top = getHeight() - getPaddingBottom() - mTabHeight; + break; + default: + mTempRectF.top = getPaddingTop(); + } + + mTempRectF.bottom = mTempRectF.top + mTabHeight; + + float tabWidth = mTabWidth; + if (fillHorizontal) { + tabWidth = (getWidth() - getPaddingRight() - getPaddingLeft() + - (mPageCount - 1) * mTabSpacing) / mPageCount; + } + + for (int i = 0; i < mPageCount; i++) { + mTempRectF.left = totalLeft + (i * (tabWidth + mTabSpacing)); + mTempRectF.right = mTempRectF.left + tabWidth; + canvas.drawRect(mTempRectF, i < mCurrentPage + ? mPrevTabPaint + : (i > mCurrentPage + ? mNextTabPaint + : (i == mPageCount - 1 + ? mSelectedLastTabPaint + : mSelectedTabPaint))); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension( + View.resolveSize( + (int) (mPageCount * (mTabWidth + mTabSpacing) - mTabSpacing) + + getPaddingLeft() + getPaddingRight(), + widthMeasureSpec + ), + View.resolveSize( + (int) mTabHeight + + getPaddingTop() + getPaddingBottom(), + heightMeasureSpec + )); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + scrollCurrentPageIntoView(); + super.onSizeChanged(w, h, oldw, oldh); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (mOnPageSelectedListener != null) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: + int position = hitTest(event.getX()); + if (position >= 0) { + mOnPageSelectedListener.onPageStripSelected(position); + } + return true; + } + } + return super.onTouchEvent(event); + } + + private int hitTest(float x) { + if (mPageCount == 0) { + return -1; + } + + float totalWidth = mPageCount * (mTabWidth + mTabSpacing) - mTabSpacing; + float totalLeft; + boolean fillHorizontal = false; + + switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.CENTER_HORIZONTAL: + totalLeft = (getWidth() - totalWidth) / 2; + break; + case Gravity.RIGHT: + totalLeft = getWidth() - getPaddingRight() - totalWidth; + break; + case Gravity.FILL_HORIZONTAL: + totalLeft = getPaddingLeft(); + fillHorizontal = true; + break; + default: + totalLeft = getPaddingLeft(); + } + + float tabWidth = mTabWidth; + if (fillHorizontal) { + tabWidth = (getWidth() - getPaddingRight() - getPaddingLeft() + - (mPageCount - 1) * mTabSpacing) / mPageCount; + } + + float totalRight = totalLeft + (mPageCount * (tabWidth + mTabSpacing)); + if (x >= totalLeft && x <= totalRight && totalRight > totalLeft) { + return (int) (((x - totalLeft) / (totalRight - totalLeft)) * mPageCount); + } else { + return -1; + } + } + + public void setCurrentPage(int currentPage) { + mCurrentPage = currentPage; + invalidate(); + scrollCurrentPageIntoView(); + + // TODO: Set content description appropriately + } + + private void scrollCurrentPageIntoView() { + // TODO: only works with left gravity for now +// +// float widthToActive = getPaddingLeft() + (mCurrentPage + 1) * (mTabWidth + mTabSpacing) +// - mTabSpacing; +// int viewWidth = getWidth(); +// +// int startScrollX = getScrollX(); +// int destScrollX = (widthToActive > viewWidth) ? (int) (widthToActive - viewWidth) : 0; +// +// if (mScroller == null) { +// mScroller = new Scroller(getContext()); +// } +// +// mScroller.abortAnimation(); +// mScroller.startScroll(startScrollX, 0, destScrollX - startScrollX, 0); +// postInvalidate(); + } + + public void setPageCount(int count) { + mPageCount = count; + invalidate(); + + // TODO: Set content description appropriately + } + + public static interface OnPageSelectedListener { + void onPageStripSelected(int position); + } + +// +// @Override +// public void computeScroll() { +// super.computeScroll(); +// if (mScroller.computeScrollOffset()) { +// setScrollX(mScroller.getCurrX()); +// } +// } +} diff --git a/flock/src/main/java/de/passsy/holocircularprogressbar/HoloCircularProgressBar.java b/flock/src/main/java/de/passsy/holocircularprogressbar/HoloCircularProgressBar.java new file mode 100644 index 0000000..44b37e8 --- /dev/null +++ b/flock/src/main/java/de/passsy/holocircularprogressbar/HoloCircularProgressBar.java @@ -0,0 +1,654 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +/** + * + */ +package de.passsy.holocircularprogressbar; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; + +import org.anhonesteffort.flock.R; + +/** + * The Class HoloCircularProgressBar. + * + * @author Pascal.Welsch + * @since 05.03.2013 + * + * @version 1.1 (12.10.2013) + */ +public class HoloCircularProgressBar extends View { + + /** + * The Constant TAG. + */ + private static final String TAG = HoloCircularProgressBar.class.getSimpleName(); + + /** + * used to save the super state on configuration change + */ + private static final String INSTANCE_STATE_SAVEDSTATE = "saved_state"; + + /** + * used to save the progress on configuration changes + */ + private static final String INSTANCE_STATE_PROGRESS = "progress"; + + /** + * used to save the marker progress on configuration changes + */ + private static final String INSTANCE_STATE_MARKER_PROGRESS = "marker_progress"; + + /** + * used to save the background color of the progress + */ + private static final String INSTANCE_STATE_PROGRESS_BACKGROUND_COLOR = "progress_background_color"; + + /** + * used to save the color of the progress + */ + private static final String INSTANCE_STATE_PROGRESS_COLOR = "progress_color"; + + /** + * true if not all properties are set. then the view isn't drawn and there + * are no errors in the LayoutEditor + */ + private boolean mIsInitializing = true; + + /** + * the paint for the background. + */ + private Paint mBackgroundColorPaint = new Paint(); + + /** + * The stroke width used to paint the circle. + */ + private int mCircleStrokeWidth = 10; + + /** + * The pointer width (in pixels). + */ + private int mThumbRadius = 20; + + /** + * The rectangle enclosing the circle. + */ + private final RectF mCircleBounds = new RectF(); + + /** + * Radius of the circle + * + *

+ * Note: (Re)calculated in {@link #onMeasure(int, int)}. + *

+ */ + private float mRadius; + + /** + * the color of the progress. + */ + private int mProgressColor; + + /** + * paint for the progress. + */ + private Paint mProgressColorPaint; + + /** + * The color of the progress background. + */ + private int mProgressBackgroundColor; + + /** + * The current progress. + */ + private float mProgress = 0.3f; + + /** + * The Thumb color paint. + */ + private Paint mThumbColorPaint = new Paint(); + + /** + * The Marker progress. + */ + private float mMarkerProgress = 0.0f; + + /** + * The Marker color paint. + */ + private Paint mMarkerColorPaint; + + /** + * flag if the marker should be visible + */ + private boolean mIsMarkerEnabled = false; + + /** + * The gravity of the view. Where should the Circle be drawn within the + * given bounds + * + * {@link #computeInsets(int, int)} + */ + private final int mGravity; + + /** + * The Horizontal inset calcualted in {@link #computeInsets(int, int)} + * depends on {@link #mGravity}. + */ + private int mHorizontalInset = 0; + + /** + * The Vertical inset calcualted in {@link #computeInsets(int, int)} depends + * on {@link #mGravity}.. + */ + private int mVerticalInset = 0; + + /** + * The Translation offset x which gives us the ability to use our own + * coordinates system. + */ + private float mTranslationOffsetX; + + /** + * The Translation offset y which gives us the ability to use our own + * coordinates system. + */ + private float mTranslationOffsetY; + + /** + * The Thumb pos x. + * + * Care. the position is not the position of the rotated thumb. The position + * is only calculated in {@link #onMeasure(int, int)} + */ + private float mThumbPosX; + + /** + * The Thumb pos y. + * + * Care. the position is not the position of the rotated thumb. The position + * is only calculated in {@link #onMeasure(int, int)} + */ + private float mThumbPosY; + + /** + * the overdraw is true if the progress is over 1.0. + * + */ + private boolean mOverrdraw = false; + + /** + * the rect for the thumb square + */ + private final RectF mSquareRect = new RectF(); + + /** + * indicates if the thumb is visible + */ + private boolean mIsThumbEnabled = true; + + /** + * Instantiates a new holo circular progress bar. + * + * @param context + * the context + */ + public HoloCircularProgressBar(final Context context) { + this(context, null); + } + + /** + * Instantiates a new holo circular progress bar. + * + * @param context + * the context + * @param attrs + * the attrs + */ + public HoloCircularProgressBar(final Context context, final AttributeSet attrs) { + this(context, attrs, R.attr.circularProgressBarStyle); + } + + /** + * Instantiates a new holo circular progress bar. + * + * @param context + * the context + * @param attrs + * the attrs + * @param defStyle + * the def style + */ + public HoloCircularProgressBar(final Context context, final AttributeSet attrs, final int defStyle) { + super(context, attrs, defStyle); + + // load the styled attributes and set their properties + final TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.HoloCircularProgressBar, + defStyle, 0); + + setProgressColor(attributes.getColor(R.styleable.HoloCircularProgressBar_progress_color, Color.CYAN)); + setProgressBackgroundColor(attributes.getColor(R.styleable.HoloCircularProgressBar_progress_background_color, + Color.MAGENTA)); + setProgress(attributes.getFloat(R.styleable.HoloCircularProgressBar_progress, 0.0f)); + setMarkerProgress(attributes.getFloat(R.styleable.HoloCircularProgressBar_marker_progress, 0.0f)); + setWheelSize((int) attributes.getDimension(R.styleable.HoloCircularProgressBar_stroke_width, 10)); + mIsThumbEnabled = attributes.getBoolean(R.styleable.HoloCircularProgressBar_thumb_visible, true); + mIsMarkerEnabled = attributes.getBoolean(R.styleable.HoloCircularProgressBar_marker_visible, true); + + mGravity = attributes.getInt(R.styleable.HoloCircularProgressBar_android_gravity, Gravity.CENTER); + + attributes.recycle(); + + mThumbRadius = mCircleStrokeWidth * 2; + + updateBackgroundColor(); + + updateMarkerColor(); + + updateProgressColor(); + + // the view has now all properties and can be drawn + mIsInitializing = false; + + } + + /* + * (non-Javadoc) + * + * @see android.view.View#onDraw(android.graphics.Canvas) + */ + @Override + protected void onDraw(final Canvas canvas) { + + // All of our positions are using our internal coordinate system. + // Instead of translating + // them we let Canvas do the work for us. + canvas.translate(mTranslationOffsetX, mTranslationOffsetY); + + final float progressRotation = getCurrentRotation(); + + // draw the background + if (!mOverrdraw) { + canvas.drawArc(mCircleBounds, 270, -(360 - progressRotation), false, mBackgroundColorPaint); + } + + // draw the progress or a full circle if overdraw is true + canvas.drawArc(mCircleBounds, 270, mOverrdraw ? 360 : progressRotation, false, mProgressColorPaint); + + // draw the marker at the correct rotated position + if (mIsMarkerEnabled) { + final float markerRotation = getMarkerRotation(); + + canvas.save(); + canvas.rotate(markerRotation - 90); + canvas.drawLine((float) (mThumbPosX + mThumbRadius / 2 * 1.4), mThumbPosY, + (float) (mThumbPosX - mThumbRadius / 2 * 1.4), mThumbPosY, mMarkerColorPaint); + canvas.restore(); + } + + if (isThumbEnabled()) { + // draw the thumb square at the correct rotated position + canvas.save(); + canvas.rotate(progressRotation - 90); + // rotate the square by 45 degrees + canvas.rotate(45, mThumbPosX, mThumbPosY); + mSquareRect.left = mThumbPosX - mThumbRadius / 3; + mSquareRect.right = mThumbPosX + mThumbRadius / 3; + mSquareRect.top = mThumbPosY - mThumbRadius / 3; + mSquareRect.bottom = mThumbPosY + mThumbRadius / 3; + canvas.drawRect(mSquareRect, mThumbColorPaint); + canvas.restore(); + } + } + + /* + * (non-Javadoc) + * + * @see android.view.View#onMeasure(int, int) + */ + @Override + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { + final int height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec); + final int width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec); + final int min = Math.min(width, height); + setMeasuredDimension(min, height); + + final float halfWidth = min * 0.5f; + mRadius = halfWidth - mThumbRadius; + + mCircleBounds.set(-mRadius, -mRadius, mRadius, mRadius); + + mThumbPosX = (float) (mRadius * Math.cos(0)); + mThumbPosY = (float) (mRadius * Math.sin(0)); + computeInsets(width - min, height - min); + + mTranslationOffsetX = halfWidth + mHorizontalInset; + mTranslationOffsetY = halfWidth + mVerticalInset; + + } + + /* + * (non-Javadoc) + * + * @see android.view.View#onRestoreInstanceState(android.os.Parcelable) + */ + @Override + protected void onRestoreInstanceState(final Parcelable state) { + if (state instanceof Bundle) { + final Bundle bundle = (Bundle) state; + setProgress(bundle.getFloat(INSTANCE_STATE_PROGRESS)); + setMarkerProgress(bundle.getFloat(INSTANCE_STATE_MARKER_PROGRESS)); + + final int progressColor = bundle.getInt(INSTANCE_STATE_PROGRESS_COLOR); + if (progressColor != mProgressColor) { + mProgressColor = progressColor; + updateProgressColor(); + } + + final int progressBackgroundColor = bundle.getInt(INSTANCE_STATE_PROGRESS_BACKGROUND_COLOR); + if (progressBackgroundColor != mProgressBackgroundColor) { + mProgressBackgroundColor = progressBackgroundColor; + updateBackgroundColor(); + } + + super.onRestoreInstanceState(bundle.getParcelable(INSTANCE_STATE_SAVEDSTATE)); + return; + } + + super.onRestoreInstanceState(state); + } + + /* + * (non-Javadoc) + * + * @see android.view.View#onSaveInstanceState() + */ + @Override + protected Parcelable onSaveInstanceState() { + final Bundle bundle = new Bundle(); + bundle.putParcelable(INSTANCE_STATE_SAVEDSTATE, super.onSaveInstanceState()); + bundle.putFloat(INSTANCE_STATE_PROGRESS, mProgress); + bundle.putFloat(INSTANCE_STATE_MARKER_PROGRESS, mMarkerProgress); + bundle.putInt(INSTANCE_STATE_PROGRESS_COLOR, mProgressColor); + bundle.putInt(INSTANCE_STATE_PROGRESS_BACKGROUND_COLOR, mProgressBackgroundColor); + return bundle; + } + + /** + * Compute insets. + * + *
+	 *  ______________________
+	 * |_________dx/2_________|
+	 * |......| /'''''\|......|
+	 * |-dx/2-|| View ||-dx/2-|
+	 * |______| \_____/|______|
+	 * |________ dx/2_________|
+	 * 
+ * + * @param dx + * the dx the horizontal unfilled space + * @param dy + * the dy the horizontal unfilled space + */ + @SuppressLint("NewApi") + private void computeInsets(final int dx, final int dy) { + final int layoutDirection; + int absoluteGravity = mGravity; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + layoutDirection = getLayoutDirection(); + absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection); + } + + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.LEFT: + mHorizontalInset = 0; + break; + case Gravity.RIGHT: + mHorizontalInset = dx; + break; + case Gravity.CENTER_HORIZONTAL: + default: + mHorizontalInset = dx / 2; + break; + } + switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) { + case Gravity.TOP: + mVerticalInset = 0; + break; + case Gravity.BOTTOM: + mVerticalInset = dy; + break; + case Gravity.CENTER_VERTICAL: + default: + mVerticalInset = dy / 2; + break; + } + } + + /** + * Gets the current rotation. + * + * @return the current rotation + */ + private float getCurrentRotation() { + return 360 * mProgress; + } + + /** + * Gets the marker rotation. + * + * @return the marker rotation + */ + private float getMarkerRotation() { + + return 360 * mMarkerProgress; + } + + /** + * Sets the wheel size. + * + * @param dimension + * the new wheel size + */ + private void setWheelSize(final int dimension) { + mCircleStrokeWidth = dimension; + } + + /** + * updates the paint of the background + */ + private void updateBackgroundColor() { + mBackgroundColorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mBackgroundColorPaint.setColor(mProgressBackgroundColor); + mBackgroundColorPaint.setStyle(Paint.Style.STROKE); + mBackgroundColorPaint.setStrokeWidth(mCircleStrokeWidth); + + invalidate(); + } + + /** + * updates the paint of the marker + */ + private void updateMarkerColor() { + mMarkerColorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mMarkerColorPaint.setColor(mProgressBackgroundColor); + mMarkerColorPaint.setStyle(Paint.Style.STROKE); + mMarkerColorPaint.setStrokeWidth(mCircleStrokeWidth / 2); + + invalidate(); + } + + /** + * updates the paint of the progress and the thumb to give them a new visual + * style + */ + private void updateProgressColor() { + mProgressColorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mProgressColorPaint.setColor(mProgressColor); + mProgressColorPaint.setStyle(Paint.Style.STROKE); + mProgressColorPaint.setStrokeWidth(mCircleStrokeWidth); + + mThumbColorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mThumbColorPaint.setColor(mProgressColor); + mThumbColorPaint.setStyle(Paint.Style.FILL_AND_STROKE); + mThumbColorPaint.setStrokeWidth(mCircleStrokeWidth); + + invalidate(); + } + + /** + * similar to {@link getProgress} + * + * @return + */ + public float getMarkerProgress() { + return mMarkerProgress; + } + + /** + * gives the current progress of the ProgressBar. Value between 0..1 if you + * set the progress to >1 you'll get progress % 1 as return value + * + * @return the progress + */ + public float getProgress() { + return mProgress; + } + + /** + * Gets the progress color. + * + * @return the progress color + */ + public int getProgressColor() { + return mProgressColor; + } + + /** + * + * @return true if the marker is visible + */ + public boolean isMarkerEnabled() { + return mIsMarkerEnabled; + } + + /** + * + * @return true if the marker is visible + */ + public boolean isThumbEnabled() { + return mIsThumbEnabled; + } + + /** + * Sets the marker enabled. + * + * @param enabled + * the new marker enabled + */ + public void setMarkerEnabled(final boolean enabled) { + mIsMarkerEnabled = enabled; + } + + /** + * Sets the marker progress. + * + * @param progress + * the new marker progress + */ + public void setMarkerProgress(final float progress) { + mIsMarkerEnabled = true; + mMarkerProgress = progress; + } + + /** + * Sets the progress. + * + * @param progress + * the new progress + */ + public void setProgress(final float progress) { + if (progress == mProgress) { + return; + } + + if (progress == 1) { + mOverrdraw = false; + mProgress = 1; + } else { + + if (progress >= 1) { + mOverrdraw = true; + } else { + mOverrdraw = false; + } + + mProgress = progress % 1.0f; + } + + if (!mIsInitializing) { + invalidate(); + } + } + + /** + * Sets the progress background color. + * + * @param color + * the new progress background color + */ + public void setProgressBackgroundColor(final int color) { + mProgressBackgroundColor = color; + + updateMarkerColor(); + updateBackgroundColor(); + } + + /** + * Sets the progress color. + * + * @param color + * the new progress color + */ + public void setProgressColor(final int color) { + mProgressColor = color; + + updateProgressColor(); + } + + public void setThumbEnabled(final boolean enabled) { + mIsThumbEnabled = enabled; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/AbstractDavCollectionArrayAdapter.java b/flock/src/main/java/org/anhonesteffort/flock/AbstractDavCollectionArrayAdapter.java new file mode 100644 index 0000000..9bfc5f4 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/AbstractDavCollectionArrayAdapter.java @@ -0,0 +1,196 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.content.Context; +import android.os.RemoteException; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.CompoundButton; +import android.widget.TextView; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.crypto.InvalidMacException; +import org.anhonesteffort.flock.sync.HidingDavCollection; +import org.anhonesteffort.flock.sync.LocalComponentStore; +import org.anhonesteffort.flock.sync.addressbook.AddressbookSyncScheduler; +import org.anhonesteffort.flock.webdav.PropertyParseException; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.List; + +/** + * Programmer: rhodey + */ +public abstract class AbstractDavCollectionArrayAdapter + extends ArrayAdapter +{ + + protected LayoutInflater inflater; + protected int rowLayout; + protected T[] remoteCollections; + protected LocalComponentStore localStore; + protected List batchSelections; + + public AbstractDavCollectionArrayAdapter(Context context, + int rowLayout, + T[] remoteCollections, + LocalComponentStore localStore, + List batchSelections) + { + super(context, rowLayout, remoteCollections); + + this.rowLayout = rowLayout; + this.remoteCollections = remoteCollections; + this.localStore = localStore; + this.batchSelections = batchSelections; + + inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + protected abstract void handlePopulateView(int position, ViewHolder viewHolder); + + protected class ViewHolder { + + public ViewHolder() { } + + public ViewHolder(ViewHolder viewHolder) { + this.displayName = viewHolder.displayName; + this.syncCheck = viewHolder.syncCheck; + } + + public TextView displayName; + public CompoundButton syncCheck; + } + + protected ViewHolder getViewHolder(View collectionRowView) { + ViewHolder viewHolder = new ViewHolder(); + viewHolder.displayName = (TextView) collectionRowView.findViewById(R.id.collection_display_name); + viewHolder.syncCheck = (CompoundButton) collectionRowView.findViewById(R.id.collection_sync_button); + + return viewHolder; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View collectionRowView = convertView; + + if (convertView == null) { + collectionRowView = inflater.inflate(rowLayout, parent, false); + collectionRowView.setTag(R.integer.tag_view_holder, getViewHolder(collectionRowView)); + } + + ViewHolder viewHolder = (ViewHolder) collectionRowView.getTag(R.integer.tag_view_holder); + + viewHolder.syncCheck.setOnCheckedChangeListener(getOnCheckChangedListener(remoteCollections[position])); + collectionRowView.setTag(R.integer.tag_collection_path, remoteCollections[position].getPath()); + + if (batchSelections.contains(remoteCollections[position].getPath())) { + collectionRowView.setTag(R.integer.tag_collection_selected, Boolean.TRUE); + collectionRowView.setBackgroundResource(R.color.holo_blue_dark); + } + else { + collectionRowView.setTag(R.integer.tag_collection_selected, Boolean.FALSE); + collectionRowView.setBackgroundResource(0); + } + + viewHolder.displayName.setText(R.string.display_name_unavailable); + + try { + + Optional displayName = remoteCollections[position].getHiddenDisplayName(); + if (displayName.isPresent()) + viewHolder.displayName.setText(displayName.get()); + else + viewHolder.displayName.setText(R.string.display_name_missing); + + if (localStore.getCollection(remoteCollections[position].getPath()).isPresent()) + viewHolder.syncCheck.setChecked(true); + else + viewHolder.syncCheck.setChecked(false); + + } catch (PropertyParseException e) { + ErrorToaster.handleShowError(getContext(), e); + } catch (RemoteException e) { + ErrorToaster.handleShowError(getContext(), e); + } catch (InvalidMacException e) { + ErrorToaster.handleShowError(getContext(), e); + } catch (GeneralSecurityException e) { + ErrorToaster.handleShowError(getContext(), e); + } catch (IOException e) { + ErrorToaster.handleShowError(getContext(), e); + } + + handlePopulateView(position, viewHolder); + + return collectionRowView; + } + + protected CompoundButton.OnCheckedChangeListener getOnCheckChangedListener(T remoteCollection) { + return new SyncChangeListener(remoteCollection); + } + + private class SyncChangeListener implements CompoundButton.OnCheckedChangeListener { + + protected T remoteCollection; + + public SyncChangeListener(T remoteCollection) { + this.remoteCollection = remoteCollection; + } + + @Override + public void onCheckedChanged(CompoundButton checkBoxView, boolean isChecked) { + if (!checkBoxView.isShown()) + return; + + try { + + if (isChecked) { + Optional displayName = remoteCollection.getHiddenDisplayName(); + if (displayName.isPresent()) + localStore.addCollection(remoteCollection.getPath(), displayName.get()); + else { + localStore.addCollection(remoteCollection.getPath(), + getContext().getString(R.string.display_name_missing)); + } + } + else + localStore.removeCollection(remoteCollection.getPath()); + + new AddressbookSyncScheduler(getContext()).requestSync(); + + } catch (PropertyParseException e) { + ErrorToaster.handleShowError(getContext(), e); + } catch (InvalidMacException e) { + ErrorToaster.handleShowError(getContext(), e); + } catch (GeneralSecurityException e) { + ErrorToaster.handleShowError(getContext(), e); + } catch (RemoteException e) { + ErrorToaster.handleShowError(getContext(), e); + } catch (IOException e) { + ErrorToaster.handleShowError(getContext(), e); + } + } + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/AbstractMyCollectionsFragment.java b/flock/src/main/java/org/anhonesteffort/flock/AbstractMyCollectionsFragment.java new file mode 100644 index 0000000..37d4559 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/AbstractMyCollectionsFragment.java @@ -0,0 +1,264 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.os.AsyncTask; +import android.os.Bundle; +import android.util.Log; +import android.view.ActionMode; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.CompoundButton; +import android.widget.ListView; +import android.widget.TextView; + +import com.google.common.base.Optional; + +import java.util.LinkedList; +import java.util.List; + +/** + * Programmer: rhodey + */ +public abstract class AbstractMyCollectionsFragment extends AccountAndKeyRequiredFragment + implements ActionMode.Callback, ListView.OnItemClickListener, ListView.OnItemLongClickListener +{ + + private static final String TAG = "org.anhonesteffort.flock.AbstractMyCollectionsFragment"; + + protected Activity activity; + protected AsyncTask asyncTask; + protected ListView collectionsListView; + private Menu optionsMenu; + protected ActionMode actionMode; + protected AlertDialog alertDialog; + + protected Optional setupActivity = Optional.absent(); + protected List batchSelections; + + protected boolean list_is_initializing = false; + protected boolean progress_is_shown = false; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + if (activity instanceof SetupActivity) + this.setupActivity = Optional.of((SetupActivity) activity); + } + + @Override + public View onCreateView(LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState) + { + activity = getActivity(); + View fragmentView = inflater.inflate(R.layout.fragment_list_sync_collections, container, false); + + if (accountAndKeyAvailable()) + initButtons(); + + return fragmentView; + } + + protected abstract void handleButtonNext(); + + private void initButtons() { + if (activity.findViewById(R.id.button_next) == null) + return; + + activity.findViewById(R.id.button_next).setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View view) { + if (setupActivity.isPresent()) + handleButtonNext(); + } + + }); + } + + @Override + public void onResume() { + super.onResume(); + + activity = getActivity(); + initializeList(); + } + + @Override + public void onPause() { + super.onPause(); + + if (alertDialog != null) + alertDialog.dismiss(); + + if (asyncTask != null && !asyncTask.isCancelled()) { + asyncTask.cancel(true); + asyncTask = null; + } + } + + protected void initializeList() { + Log.d(TAG, "initializeList()"); + + if (list_is_initializing || account == null) + return; + + batchSelections = new LinkedList(); + updateActionMode(); + + list_is_initializing = true; + retrieveRemoteCollectionsAsync(); + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + actionMode = null; + batchSelections.clear(); + + for(int i = 0; i < collectionsListView.getChildCount(); i++) + handleUnselectRow(collectionsListView.getChildAt(i)); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (!view.isShown() || progress_is_shown) + return; + + if (batchSelections.size() == 0) { + CompoundButton syncCheckbox = (CompoundButton) view.findViewById(R.id.collection_sync_button); + syncCheckbox.setChecked(!syncCheckbox.isChecked()); + } + else { + if (view.getTag(R.integer.tag_collection_selected) == Boolean.TRUE) + handleUnselectRow(view); + else + handleSelectRow(view); + } + } + + @Override + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + if (!view.isShown() || progress_is_shown) + return true; + + if (batchSelections.size() == 0) + handleSelectRow(view); + + return true; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + optionsMenu = menu; + } + + protected abstract void handleHideOptionsMenuItems(Menu menu); + + protected abstract void handleRestoreOptionsMenuItems(Menu menu); + + protected void handleStartIndeterminateProgress() { + if (optionsMenu != null) + handleHideOptionsMenuItems(optionsMenu); + + activity.setProgressBarIndeterminateVisibility(true); + activity.setProgressBarVisibility(true); + progress_is_shown = true; + } + + protected void handleStopIndeterminateProgress() { + activity.setProgressBarIndeterminateVisibility(false); + activity.setProgressBarVisibility(false); + progress_is_shown = false; + + if (optionsMenu != null) + handleRestoreOptionsMenuItems(optionsMenu); + } + + protected abstract String getStringCollectionsSelected(); + + protected void updateActionMode() { + if (actionMode != null) { + if (batchSelections.size() == 0) + actionMode.finish(); + + else { + actionMode.getMenu().clear(); + actionMode.setSubtitle(batchSelections.size() + " " + getStringCollectionsSelected()); + + if (batchSelections.size() == 1) + actionMode.getMenuInflater().inflate(R.menu.collection_list_edit, actionMode.getMenu()); + else + actionMode.getMenuInflater().inflate(R.menu.collection_list_delete, actionMode.getMenu()); + } + } + else if (batchSelections.size() > 0) + actionMode = activity.startActionMode(this); + } + + protected void handleSelectRow(View view) { + batchSelections.add((String) view.getTag(R.integer.tag_collection_path)); + view.setTag(R.integer.tag_collection_selected, Boolean.TRUE); + view.setBackgroundResource(R.color.holo_blue_dark); + + updateActionMode(); + } + + protected void handleUnselectRow(View view) { + batchSelections.remove((String) view.getTag(R.integer.tag_collection_path)); + view.setTag(R.integer.tag_collection_selected, Boolean.FALSE); + view.setBackgroundResource(0); + + updateActionMode(); + } + + protected Optional getDisplayNameForSelectedCollection() { + if (batchSelections.size() == 0) + return Optional.absent(); + + for(int i = 0; i < collectionsListView.getChildCount(); i++) { + View rowView = collectionsListView.getChildAt(i); + TextView displayNameView = (TextView) rowView.findViewById(R.id.collection_display_name); + Boolean selected = (Boolean ) rowView.getTag(R.integer.tag_collection_selected); + + if (selected && displayNameView.getText() != null) + return Optional.of(displayNameView.getText().toString()); + } + + return Optional.absent(); + } + + protected abstract void retrieveRemoteCollectionsAsync(); + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/AccountAndKeyRequiredActivity.java b/flock/src/main/java/org/anhonesteffort/flock/AccountAndKeyRequiredActivity.java new file mode 100644 index 0000000..4f5ef88 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/AccountAndKeyRequiredActivity.java @@ -0,0 +1,104 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.widget.Toast; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.crypto.KeyHelper; +import org.anhonesteffort.flock.crypto.MasterCipher; + +import java.io.IOException; + +/** + * Programmer: rhodey + */ +public class AccountAndKeyRequiredActivity extends Activity { + + private static final String TAG = "org.anhonesteffort.flock.AccountAndKeyRequiredActivity"; + + protected DavAccount account; + protected MasterCipher masterCipher; + + protected static DavAccount handleGetAccountOrFail(Activity activity) { + Intent nextIntent; + Optional davAccount = DavAccountHelper.getAccount(activity); + + if (!davAccount.isPresent()) { + if (!DavAccountHelper.isAccountRegistered(activity)) { + Log.w(TAG, "dav account missing and account not registered, directing to setup activity"); + nextIntent = new Intent(activity, SetupActivity.class); + nextIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + + else { + Log.w(TAG, "dav account missing and account is registered, directing to correct password"); + nextIntent = new Intent(activity, CorrectPasswordActivity.class); + nextIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Toast.makeText(activity, + R.string.error_password_unavailable_please_login, + Toast.LENGTH_SHORT).show(); + } + + activity.startActivity(nextIntent); + activity.finish(); + return null; + } + + else + return davAccount.get(); + } + + protected static MasterCipher handleGetMasterCipherOrFail(Activity activity) { + try { + + Optional cipher = KeyHelper.getMasterCipher(activity); + if (cipher.isPresent()) + return cipher.get(); + else { + Log.e(TAG, "master cipher is missing, fuck"); + throw new IOException("Where did master chipher GO!?!?"); + } + + } catch (IOException e) { + // TODO: import account from scratch... + activity.finish(); + return null; + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + account = handleGetAccountOrFail(this); + masterCipher = handleGetMasterCipherOrFail(this); + } + + protected boolean accountAndKeyAvailable() { + return account != null && masterCipher != null; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/AccountAndKeyRequiredFragment.java b/flock/src/main/java/org/anhonesteffort/flock/AccountAndKeyRequiredFragment.java new file mode 100644 index 0000000..59ea31c --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/AccountAndKeyRequiredFragment.java @@ -0,0 +1,48 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.app.Activity; +import android.support.v4.app.Fragment; + +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.crypto.MasterCipher; + +/** + * Programmer: rhodey + */ +public class AccountAndKeyRequiredFragment extends Fragment { + + protected DavAccount account; + protected MasterCipher masterCipher; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + account = AccountAndKeyRequiredActivity.handleGetAccountOrFail(getActivity()); + masterCipher = AccountAndKeyRequiredActivity.handleGetMasterCipherOrFail(getActivity()); + } + + protected boolean accountAndKeyAvailable() { + return account != null && masterCipher != null; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/AccountContactDetailsListAdapter.java b/flock/src/main/java/org/anhonesteffort/flock/AccountContactDetailsListAdapter.java new file mode 100644 index 0000000..8573913 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/AccountContactDetailsListAdapter.java @@ -0,0 +1,125 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.accounts.Account; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.TextView; + +import com.google.common.base.Optional; + +import java.util.List; + +/** + * Programmer: rhodey + */ +public class AccountContactDetailsListAdapter + extends ArrayAdapter + implements View.OnClickListener, CompoundButton.OnCheckedChangeListener +{ + + private ImportContactsFragment.AccountContactDetails[] accountDetails; + private List selectedAccounts; + private CompoundButton.OnCheckedChangeListener checkListener; + + public AccountContactDetailsListAdapter(Context context, + ImportContactsFragment.AccountContactDetails[] accountDetails, + List selectedAccounts, + CompoundButton.OnCheckedChangeListener checkListener) + { + super(context, R.layout.fragment_simple_list, accountDetails); + + this.accountDetails = accountDetails; + this.selectedAccounts = selectedAccounts; + this.checkListener = checkListener; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View rowView = inflater.inflate(R.layout.row_account_contact_details, parent, false); + TextView accountNameView = (TextView) rowView.findViewById(R.id.account_name); + TextView contactCountView = (TextView) rowView.findViewById(R.id.account_contact_count); + CheckBox importCheckbox = (CheckBox) rowView.findViewById(R.id.import_checkbox); + + importCheckbox.setTag(R.integer.tag_account_name, accountDetails[position].account.name); + importCheckbox.setTag(R.integer.tag_account_type, accountDetails[position].account.type); + importCheckbox.setTag(R.integer.tag_account_contact_count, accountDetails[position].contact_count); + + accountNameView.setText(accountDetails[position].account.name); + contactCountView.setText(accountDetails[position].contact_count + " " + + getContext().getString(R.string.contacts)); + + Account viewAccount = new Account(accountDetails[position].account.name, + accountDetails[position].account.type); + + for (ImportContactsFragment.AccountContactDetails selectedAccount : selectedAccounts) { + if (selectedAccount.account.equals(viewAccount)) { + importCheckbox.setChecked(true); + break; + } + } + + rowView.setOnClickListener(this); + importCheckbox.setOnCheckedChangeListener(this); + + return rowView; + } + + @Override + public void onClick(View view) { + CheckBox importCheckbox = (CheckBox) view.findViewById(R.id.import_checkbox); + importCheckbox.setChecked(!importCheckbox.isChecked()); + } + + @Override + public void onCheckedChanged(CompoundButton importCheckbox, boolean isChecked) { + String accountName = (String) importCheckbox.getTag(R.integer.tag_account_name); + String accountType = (String) importCheckbox.getTag(R.integer.tag_account_type); + Account tappedAccount = new Account(accountName, accountType); + + Optional accountDetails = Optional.absent(); + for (ImportContactsFragment.AccountContactDetails selectedAccount : selectedAccounts) { + if (selectedAccount.account.equals(tappedAccount)) { + accountDetails = Optional.of(selectedAccount); + break; + } + } + + if (!isChecked && accountDetails.isPresent()) + selectedAccounts.remove(accountDetails.get()); + else if (isChecked && !accountDetails.isPresent()) { + for (ImportContactsFragment.AccountContactDetails details : this.accountDetails) { + if (details.account.equals(tappedAccount)) { + selectedAccounts.add(details); + break; + } + } + } + + checkListener.onCheckedChanged(importCheckbox, isChecked); + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/CalendarCopyService.java b/flock/src/main/java/org/anhonesteffort/flock/CalendarCopyService.java new file mode 100644 index 0000000..2f101a7 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/CalendarCopyService.java @@ -0,0 +1,376 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.accounts.Account; +import android.app.NotificationManager; +import android.app.Service; +import android.content.ContentProviderClient; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.support.v4.app.NotificationCompat; +import android.util.Log; + +import org.anhonesteffort.flock.sync.calendar.CalendarCopiedListener; +import org.anhonesteffort.flock.sync.calendar.CalendarsSyncScheduler; +import org.anhonesteffort.flock.sync.calendar.LocalEventCollection; + +import java.util.LinkedList; +import java.util.List; + +/** + * Programmer: rhodey + */ +public class CalendarCopyService extends Service implements CalendarCopiedListener { + + private static final String TAG = "org.anhonesteffort.flock.CalendarCopyService"; + + protected static final String ACTION_QUEUE_ACCOUNT_FOR_COPY = "CalendarCopyService.ACTION_QUEUE_ACCOUNT_FOR_COPY"; + protected static final String ACTION_START_COPY = "CalendarCopyService.ACTION_START_COPY"; + + private static final String KEY_INTENT = "CalendarCopyService.KEY_INTENT"; + private static final String KEY_ACCOUNT_WITH_ERROR = "CalendarCopyService.KEY_ACCOUNT_WITH_ERROR"; + private static final String KEY_ACCOUNT_ERROR_COUNT = "CalendarCopyService.KEY_ACCOUNT_ERROR_COUNT"; + + protected static final String KEY_FROM_ACCOUNT = "CalendarCopyService.KEY_FROM_ACCOUNT"; + protected static final String KEY_TO_ACCOUNT = "CalendarCopyService.KEY_TO_ACCOUNT"; + protected static final String KEY_CALENDAR_ID = "CalendarCopyService.KEY_CALENDAR_ID"; + protected static final String KEY_CALENDAR_NAME = "CalendarCopyService.KEY_CALENDAR_NAME"; + protected static final String KEY_CALENDAR_COLOR = "CalendarCopyService.KEY_CALENDAR_COLOR"; + protected static final String KEY_EVENT_COUNT = "CalendarCopyService.KEY_EVENT_COUNT"; + + private static final int ID_CALENDAR_COPY_NOTIFICATION = 1024; + + // lol, no regrets + private final List accountsForCopy = new LinkedList(); + private final List calendarsForCopy = new LinkedList(); + + private Looper serviceLooper; + private ServiceHandler serviceHandler; + private NotificationManager notifyManager; + private NotificationCompat.Builder notificationBuilder; + private List accountErrors; + + private int countEventsToCopy = 0; + private int countEventsCopied = 0; + private int countEventCopiesFailed = 0; + + private void handleInitializeEventCopyNotification() { + Log.d(TAG, "handleInitializeEventCopyNotification()"); + + notificationBuilder.setContentTitle(getString(R.string.notification_calendar_import)) + .setContentText(getString(R.string.notification_importing_calendars)) + .setProgress(100, 1, false) + .setSmallIcon(R.drawable.flock_actionbar_icon); + + startForeground(ID_CALENDAR_COPY_NOTIFICATION, notificationBuilder.build()); + } + + private void handleCopyComplete() { + Log.d(TAG, "handleCopyComplete()"); + + new CalendarsSyncScheduler(getBaseContext()).requestSync(); + stopForeground(false); + stopSelf(); + } + + @Override + public void onDestroy() { + Log.d(TAG, "onDestroy()"); + + if (countEventCopiesFailed == 0) { + notificationBuilder + .setProgress(0, 0, false) + .setContentText(getString(R.string.notification_import_complete_copied) + + " " + countEventsCopied + " " + getString(R.string.events) + + getString(R.string.period)); + } + else { + notificationBuilder + .setProgress(0, 0, false) + .setContentText(getString(R.string.notification_import_complete_copied) + + " " + countEventCopiesFailed + " " + getString(R.string.events) + + ", " + countEventCopiesFailed + " " + getString(R.string.failed) + + getString(R.string.period)); + } + + notifyManager.notify(ID_CALENDAR_COPY_NOTIFICATION, notificationBuilder.build()); + } + + private void handleEventCopied(Account fromAccount) { + countEventsCopied++; + Log.d(TAG, "handleEventCopied() events copied: " + countEventsCopied); + + notificationBuilder + .setContentText(getString(R.string.notification_importing_events_from) + + " " + fromAccount.name) + .setProgress(countEventsToCopy, + countEventsCopied + countEventCopiesFailed, + false); + + notifyManager.notify(ID_CALENDAR_COPY_NOTIFICATION, notificationBuilder.build()); + } + + private void handleEventCopyFailed(Account fromAccount) { + countEventCopiesFailed++; + Log.d(TAG, "handleEventCopyFailed() event copies failed: " + countEventCopiesFailed); + + boolean accountWasFound = false; + for (Bundle accountError : accountErrors) { + Account accountWithError = accountError.getParcelable(KEY_ACCOUNT_WITH_ERROR); + Integer errorCount = accountError.getInt(KEY_ACCOUNT_ERROR_COUNT, -1); + + if (accountWithError != null && errorCount > 0) { + if (fromAccount.equals(accountWithError)) { + accountError.putInt(KEY_ACCOUNT_ERROR_COUNT, errorCount + 1); + accountWasFound = true; + break; + } + } + } + + if (!accountWasFound) { + Bundle errorBundle = new Bundle(); + errorBundle.putParcelable(KEY_ACCOUNT_WITH_ERROR, fromAccount); + errorBundle.putInt(KEY_ACCOUNT_ERROR_COUNT, 1); + + accountErrors.add(errorBundle); + } + + notificationBuilder + .setContentText(getString(R.string.notification_importing_events_from) + + " " + fromAccount.name) + .setProgress(countEventsToCopy, + countEventsCopied + countEventCopiesFailed, + false); + + notifyManager.notify(ID_CALENDAR_COPY_NOTIFICATION, notificationBuilder.build()); + } + + private void handleQueueCalendarForCopy(Intent intent) { + Log.d(TAG, "handleQueueCalendarForCopy()"); + + Account fromAccount = intent.getParcelableExtra(KEY_FROM_ACCOUNT); + Account toAccount = intent.getParcelableExtra(KEY_TO_ACCOUNT); + Long calendarId = intent.getLongExtra(KEY_CALENDAR_ID, -1); + String calendarName = intent.getStringExtra(KEY_CALENDAR_NAME); + Integer calendarColor = intent.getIntExtra(KEY_CALENDAR_COLOR, 0); + Integer eventCount = intent.getIntExtra(KEY_EVENT_COUNT, -1); + + if (fromAccount == null || toAccount == null || calendarId < 0 || + calendarName == null || eventCount < 0) + { + Log.e(TAG, "failed to parse to account, from account, calendar id, calendar " + + "label, calendar color or event count from intent extras."); + return; + } + + accountsForCopy.add(new AccountForCopy(fromAccount, toAccount, calendarId, calendarName, calendarColor)); + calendarsForCopy.add(new CalendarForCopy(fromAccount, calendarId, eventCount)); + countEventsToCopy += eventCount; + + Log.d(TAG, "events to copy: " + countEventsToCopy); + } + + private void handleStartCopy() { + Log.d(TAG, "handleStartCopy()"); + + handleInitializeEventCopyNotification(); + + ContentProviderClient client = getBaseContext().getContentResolver() + .acquireContentProviderClient(CalendarsSyncScheduler.CONTENT_AUTHORITY); + + for (AccountForCopy accountForCopy : accountsForCopy) { + Account fromAccount = accountForCopy.fromAccount; + Account toAccount = accountForCopy.toAccount; + Long calendarId = accountForCopy.calendarId; + String calendarName = accountForCopy.calendarName; + int calendarColor = accountForCopy.calendarColor; + + LocalEventCollection eventCollection = + new LocalEventCollection(client, fromAccount, calendarId, "hack"); + + try { + + eventCollection.copyToAccount(toAccount, calendarName, calendarColor, this); + + } catch (RemoteException e) { + ErrorToaster.handleShowError(getBaseContext(), e); + return; + } + } + + handleCopyComplete(); + } + + @Override + public void onCalendarCopied(Account fromAccount, Account toAccount, Long calendarId) { + Log.d(TAG, "onCalendarCopied() from " + fromAccount + ", " + calendarId + + " to " + toAccount); + } + + @Override + public void onCalendarCopyFailed(Exception e, + Account fromAccount, + Account toAccount, + Long localId) + { + Log.e(TAG, "onCalendarCopyFailed() from " + fromAccount + ", " + localId + + " to " + toAccount, e); + + int failedEvents = 0; + boolean calendarWasFound = false; + + for (CalendarForCopy calendarForCopy : calendarsForCopy) { + Account calendarAccount = calendarForCopy.fromAccount; + Long calendarId = calendarForCopy.calendarId; + Integer eventCount = calendarForCopy.eventCount; + + if (calendarAccount.equals(fromAccount) && calendarId.equals(localId)) { + failedEvents = eventCount; + calendarWasFound = true; + break; + } + } + + if (calendarWasFound) { + for (int i = 0; i < failedEvents; i++) + handleEventCopyFailed(fromAccount); + } + } + + @Override + public void onEventCopied(Account fromAccount, Account toAccount, Long calendarId) { + Log.d(TAG, "onEventCopied() from " + fromAccount + ", " + calendarId + + " to " + toAccount); + handleEventCopied(fromAccount); + } + + @Override + public void onEventCopyFailed(Exception e, + Account fromAccount, + Account toAccount, + Long calendarId) + { + Log.e(TAG, "onEventCopyFailed() from " + fromAccount + ", " + calendarId + + " to " + toAccount, e); + handleEventCopyFailed(fromAccount); + } + + @Override + public void onCreate() { + HandlerThread thread = new HandlerThread("CalendarCopyService", HandlerThread.NORM_PRIORITY); + thread.start(); + + serviceLooper = thread.getLooper(); + serviceHandler = new ServiceHandler(serviceLooper); + + notifyManager = (NotificationManager)getBaseContext().getSystemService(Context.NOTIFICATION_SERVICE); + notificationBuilder = new NotificationCompat.Builder(getBaseContext()); + accountErrors = new LinkedList(); + } + + private final class ServiceHandler extends Handler { + + public ServiceHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + Log.d(TAG, "handleMessage()"); + Intent intent = msg.getData().getParcelable(KEY_INTENT); + + if (intent != null && intent.getAction() != null) { + if (intent.getAction().equals(ACTION_QUEUE_ACCOUNT_FOR_COPY)) + handleQueueCalendarForCopy(intent); + else if (intent.getAction().equals(ACTION_START_COPY)) + handleStartCopy(); + else + Log.e(TAG, "received intent with unknown action"); + } + else + Log.e(TAG, "received message with null intent or intent action"); + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Message msg = serviceHandler.obtainMessage(); + msg.getData().putParcelable(KEY_INTENT, intent); + serviceHandler.sendMessage(msg); + + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + Log.d(TAG, "onBind()"); + return null; + } + + private static class AccountForCopy { + + public Account fromAccount; + public Account toAccount; + public long calendarId; + public String calendarName; + public int calendarColor; + + public AccountForCopy(Account fromAccount, + Account toAccount, + long calendarId, + String calendarName, + int calendarColor) + { + this.fromAccount = fromAccount; + this.toAccount = toAccount; + this.calendarId = calendarId; + this.calendarName = calendarName; + this.calendarColor = calendarColor; + } + + } + + private static class CalendarForCopy { + + public Account fromAccount; + public long calendarId; + public int eventCount; + + public CalendarForCopy(Account fromAccount, + long calendarId, + int eventCount) + { + this.fromAccount = fromAccount; + this.calendarId = calendarId; + this.eventCount = eventCount; + } + + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/ChangeEncryptionPasswordActivity.java b/flock/src/main/java/org/anhonesteffort/flock/ChangeEncryptionPasswordActivity.java new file mode 100644 index 0000000..274863c --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/ChangeEncryptionPasswordActivity.java @@ -0,0 +1,190 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.MenuItem; +import android.view.View; +import android.view.Window; +import android.widget.EditText; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.crypto.KeyStore; +import org.anhonesteffort.flock.sync.key.KeySyncScheduler; +import org.anhonesteffort.flock.util.PasswordUtil; + +/** + * Programmer: rhodey + */ +public class ChangeEncryptionPasswordActivity extends AccountAndKeyRequiredActivity { + + private static final String TAG = "org.anhonesteffort.flock.ChangeEncryptionPasswordActivity"; + + private TextWatcher passwordWatcher; + private TextWatcher passwordRepeatWatcher; + private boolean serviceStarted = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (!accountAndKeyAvailable()) + return; + + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + requestWindowFeature(Window.FEATURE_PROGRESS); + + setContentView(R.layout.change_encryption_password); + getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setTitle(R.string.title_change_encryption_password); + + handleInitForm(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + break; + } + + return false; + } + + private void handleInitForm() { + findViewById(R.id.change_password_button).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + handleChangeCipherPassphrase(); + } + }); + + final EditText passwordTextView = (EditText) findViewById(R.id.new_cipher_passphrase); + final EditText passwordRepeatTextView = (EditText) findViewById(R.id.new_cipher_passphrase_repeat); + final ProgressBar passwordProgressView = (ProgressBar) findViewById(R.id.progress_password_strength); + final ProgressBar passwordRepeatProgressView = (ProgressBar) findViewById(R.id.progress_password_strength_repeat); + + if (passwordWatcher != null) + passwordTextView.removeTextChangedListener(passwordWatcher); + if (passwordRepeatWatcher != null) + passwordRepeatTextView.removeTextChangedListener(passwordRepeatWatcher); + + passwordWatcher = PasswordUtil.getPasswordStrengthTextWatcher(getBaseContext(), passwordProgressView); + passwordRepeatWatcher = PasswordUtil.getPasswordStrengthTextWatcher(getBaseContext(), passwordRepeatProgressView); + + passwordTextView.addTextChangedListener(passwordWatcher); + passwordRepeatTextView.addTextChangedListener(passwordRepeatWatcher); + } + + private void handlePasswordChanged() { + Log.d(TAG, "handlePasswordChanged()"); + + Toast.makeText(getBaseContext(), R.string.encryption_password_saved, Toast.LENGTH_SHORT).show(); + new KeySyncScheduler(getBaseContext()).requestSync(); + finish(); + } + + private void handleChangeCipherPassphrase() { + if (serviceStarted) + return; + + Bundle result = new Bundle(); + String cipherPassphrase = ((TextView)findViewById(R.id.cipher_passphrase)).getText().toString().trim(); + String newCipherPassphrase = ((TextView)findViewById(R.id.new_cipher_passphrase)).getText().toString().trim(); + String newCipherPassphraseRepeat = ((TextView)findViewById(R.id.new_cipher_passphrase_repeat)).getText().toString().trim(); + + if (newCipherPassphrase.length() == 0) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SHORT_PASSWORD); + ErrorToaster.handleDisplayToastBundledError(getBaseContext(), result); + ((TextView)findViewById(R.id.new_cipher_passphrase)).setText(""); + return; + } + + if (!newCipherPassphrase.equals(newCipherPassphraseRepeat)) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_PASSWORDS_DO_NOT_MATCH); + ErrorToaster.handleDisplayToastBundledError(getBaseContext(), result); + ((TextView)findViewById(R.id.new_cipher_passphrase)).setText(""); + ((TextView)findViewById(R.id.new_cipher_passphrase_repeat)).setText(""); + return; + } + + Optional savedPassphrase = KeyStore.getMasterPassphrase(getBaseContext()); + if (!savedPassphrase.isPresent() || !savedPassphrase.get().equals(cipherPassphrase)) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_INVALID_CIPHER_PASSPHRASE); + ErrorToaster.handleDisplayToastBundledError(getBaseContext(), result); + + ((TextView)findViewById(R.id.cipher_passphrase)).setText(""); + ((TextView)findViewById(R.id.new_cipher_passphrase)).setText(""); + ((TextView)findViewById(R.id.new_cipher_passphrase_repeat)).setText(""); + return; + } + + Intent changeService = new Intent(getBaseContext(), ChangeEncryptionPasswordService.class); + + changeService.putExtra(ChangeEncryptionPasswordService.KEY_MESSENGER, new Messenger(new MessageHandler())); + changeService.putExtra(ChangeEncryptionPasswordService.KEY_OLD_MASTER_PASSPHRASE, cipherPassphrase); + changeService.putExtra(ChangeEncryptionPasswordService.KEY_NEW_MASTER_PASSPHRASE, newCipherPassphrase); + changeService.putExtra(ChangeEncryptionPasswordService.KEY_ACCOUNT, account.toBundle()); + + startService(changeService); + serviceStarted = true; + + setProgressBarIndeterminateVisibility(true); + setProgressBarVisibility(true); + } + + public class MessageHandler extends Handler { + + @Override + public void handleMessage(Message message) { + setProgressBarIndeterminateVisibility(false); + setProgressBarVisibility(false); + + if (message.arg1 == ErrorToaster.CODE_SUCCESS) + handlePasswordChanged(); + + else { + serviceStarted = false; + Bundle errorBundler = new Bundle(); + + errorBundler.putInt(ErrorToaster.KEY_STATUS_CODE, message.arg1); + ErrorToaster.handleDisplayToastBundledError(getBaseContext(), errorBundler); + + if (findViewById(R.id.cipher_passphrase) != null) { + ((TextView)findViewById(R.id.cipher_passphrase)).setText(""); + ((TextView)findViewById(R.id.new_cipher_passphrase)).setText(""); + ((TextView)findViewById(R.id.new_cipher_passphrase_repeat)).setText(""); + } + } + } + + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/ChangeEncryptionPasswordService.java b/flock/src/main/java/org/anhonesteffort/flock/ChangeEncryptionPasswordService.java new file mode 100644 index 0000000..6ce8b54 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/ChangeEncryptionPasswordService.java @@ -0,0 +1,344 @@ +package org.anhonesteffort.flock; + +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.support.v4.app.NotificationCompat; +import android.util.Log; + +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.crypto.KeyHelper; +import org.anhonesteffort.flock.crypto.KeyStore; +import org.anhonesteffort.flock.crypto.KeyUtil; +import org.anhonesteffort.flock.registration.RegistrationApi; +import org.anhonesteffort.flock.registration.RegistrationApiException; +import org.anhonesteffort.flock.sync.AbstractDavSyncAdapter; +import org.anhonesteffort.flock.sync.calendar.HidingCalDavCollection; +import org.anhonesteffort.flock.sync.key.KeySyncUtil; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.jackrabbit.webdav.DavException; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +/** + * Created by rhodey. + */ +public class ChangeEncryptionPasswordService extends Service { + + private static final String TAG = "org.anhonesteffort.flock.ChangeEncryptionPasswordService"; + + private static final String KEY_INTENT = "ChangeEncryptionPasswordService.KEY_INTENT"; + protected static final String KEY_MESSENGER = "ChangeEncryptionPasswordService.KEY_MESSENGER"; + protected static final String KEY_OLD_MASTER_PASSPHRASE = "ChangeEncryptionPasswordService.KEY_OLD_MASTER_PASSPHRASE"; + protected static final String KEY_NEW_MASTER_PASSPHRASE = "ChangeEncryptionPasswordService.KEY_NEW_MASTER_PASSPHRASE"; + protected static final String KEY_ACCOUNT = "ChangeEncryptionPasswordService.KEY_ACCOUNT"; + + private Looper serviceLooper; + private ServiceHandler serviceHandler; + private NotificationManager notifyManager; + private NotificationCompat.Builder notificationBuilder; + + private Messenger messenger; + private String oldMasterPassphrase; + private String newMasterPassphrase; + private DavAccount account; + + private int resultCode; + private boolean remoteActivityIsAlive = true; + + private void handleInitializeNotification() { + Log.d(TAG, "handleInitializeNotification()"); + + notificationBuilder.setContentTitle(getString(R.string.title_change_encryption_password)) + .setContentText(getString(R.string.updating_encryption_secrets)) + .setProgress(0, 0, true) + .setSmallIcon(R.drawable.flock_actionbar_icon); + + startForeground(9002, notificationBuilder.build()); + } + + @Override + public void onDestroy() { + Log.d(TAG, "onDestroy()"); + + if (remoteActivityIsAlive || resultCode == ErrorToaster.CODE_SUCCESS) + return; + + Bundle errorBundler = new Bundle(); + errorBundler.putInt(ErrorToaster.KEY_STATUS_CODE, resultCode); + ErrorToaster.handleDisplayToastBundledError(getBaseContext(), errorBundler); + + notificationBuilder + .setProgress(0, 0, false) + .setContentText(getString(R.string.password_change_failed)); + + notifyManager.notify(9002, notificationBuilder.build()); + } + + + private void handleChangeComplete() { + Log.d(TAG, "handleChangeComplete()"); + + if (remoteActivityIsAlive || resultCode == ErrorToaster.CODE_SUCCESS) + stopForeground(true); + else + stopForeground(false); + + stopSelf(); + } + + private void handleChangeOwsAuthToken(Bundle result, String passphrase) { + Log.d(TAG, "handleChangeOwsAuthToken()"); + RegistrationApi registrationApi = new RegistrationApi(getBaseContext()); + + try { + + String newAuthToken = KeyUtil.getAuthTokenForPassphrase(passphrase); + + registrationApi.setAccountPassword(account, newAuthToken); + DavAccountHelper.setAccountPassword(getBaseContext(), newAuthToken); + AbstractDavSyncAdapter.disableAuthNotificationsForRunningAdapters(getBaseContext(), account.getOsAccount()); + + account = new DavAccount(account.getUserId(), newAuthToken, account.getDavHostHREF()); + + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (RegistrationApiException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } catch (GeneralSecurityException e) { + ErrorToaster.handleBundleError(e, result); + } + } + + private String handleUpdateMasterPassphrase(Bundle result) { + Log.d(TAG, "handleUpdateMasterPassphrase()"); + + Optional oldEncryptedKeyMaterial = KeyStore.getEncryptedKeyMaterial(getBaseContext()); + if (!oldEncryptedKeyMaterial.isPresent()) { + Log.e(TAG, "old encrypted key material is missing"); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_CRYPTO_ERROR); + return null; + } + + KeyStore.saveMasterPassphrase(getBaseContext(), newMasterPassphrase); + + try { + + Optional encryptedKeyMaterial = KeyHelper.buildEncryptedKeyMaterial(getBaseContext()); + if (encryptedKeyMaterial.isPresent()) { + KeyStore.saveEncryptedKeyMaterial(getBaseContext(), encryptedKeyMaterial.get()); + + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + return encryptedKeyMaterial.get(); + } + + else { + Log.e(TAG, "new encrypted key material is missing"); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_CRYPTO_ERROR); + } + + } catch (GeneralSecurityException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) != ErrorToaster.CODE_SUCCESS) { + Log.w(TAG, "something went wrong, reverting to old passphrase"); + KeyStore.saveMasterPassphrase(getBaseContext(), oldMasterPassphrase); + KeyStore.saveEncryptedKeyMaterial(getBaseContext(), oldEncryptedKeyMaterial.get()); + } + + return null; + } + + private void handleUpdateRemoteKeyMaterial(Bundle result, String encryptedKeyMaterial) { + Log.d(TAG, "handleUpdateRemoteKeyMaterial()"); + + try { + + HidingCalDavCollection keyCollection = KeySyncUtil.getOrCreateKeyCollection(getBaseContext(), account); + keyCollection.setEncryptedKeyMaterial(encryptedKeyMaterial); + keyCollection.getStore().closeHttpConnection(); + + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (PropertyParseException e) { + ErrorToaster.handleBundleError(e, result); + } catch (DavException e) { + ErrorToaster.handleBundleError(e, result); + } catch (GeneralSecurityException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + } + + private void handleRevertLocalKeyMaterial(Bundle result) { + Log.w(TAG, "handleRevertLocalKeyMaterial()"); + + KeyStore.saveMasterPassphrase(getBaseContext(), oldMasterPassphrase); + + try { + + Optional encryptedKeyMaterial = KeyHelper.buildEncryptedKeyMaterial(getBaseContext()); + if (encryptedKeyMaterial.isPresent()) + KeyStore.saveEncryptedKeyMaterial(getBaseContext(), encryptedKeyMaterial.get()); + + else { + Log.e(TAG, "old, reverted encrypted key material is missing!!! XXX :("); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_CRYPTO_ERROR); + DavAccountHelper.invalidateAccount(getBaseContext()); + KeyStore.invalidateKeyMaterial(getBaseContext()); + } + + } catch (GeneralSecurityException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + } + + private void handleRevertOwsAuthToken(Bundle result) { + Log.w(TAG, "handleRevertOwsAuthToken()"); + int statusSave = result.getInt(ErrorToaster.KEY_STATUS_CODE); + + handleChangeOwsAuthToken(result, oldMasterPassphrase); + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) != ErrorToaster.CODE_SUCCESS) { + Log.e(TAG, "unable to revert OWS auth token!!! XXX :("); + DavAccountHelper.invalidateAccount(getBaseContext()); + KeyStore.invalidateKeyMaterial(getBaseContext()); + } + + result.putInt(ErrorToaster.KEY_STATUS_CODE, statusSave); + } + + private void handleStartChangeEncryptionPassword() { + Log.d(TAG, "handleStartChangeEncryptionPassword()"); + + Bundle result = new Bundle(); + handleInitializeNotification(); + + if (DavAccountHelper.isUsingOurServers(account)) { + handleChangeOwsAuthToken(result, newMasterPassphrase); + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + + String encryptedKeyMaterial = handleUpdateMasterPassphrase(result); + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + + handleUpdateRemoteKeyMaterial(result, encryptedKeyMaterial); + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) != ErrorToaster.CODE_SUCCESS) { + handleRevertOwsAuthToken(result); + handleRevertLocalKeyMaterial(result); + } + } + else + handleRevertOwsAuthToken(result); + } + } + + else { + String encryptedKeyMaterial = handleUpdateMasterPassphrase(result); + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + handleUpdateRemoteKeyMaterial(result, encryptedKeyMaterial); + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) != ErrorToaster.CODE_SUCCESS) + handleRevertLocalKeyMaterial(result); + } + } + + Message message = Message.obtain(); + message.arg1 = result.getInt(ErrorToaster.KEY_STATUS_CODE); + resultCode = result.getInt(ErrorToaster.KEY_STATUS_CODE); + + try { + + messenger.send(message); + + } catch (RemoteException e) { + Log.e(TAG, "caught exception while sending message to activity >> ", e); + remoteActivityIsAlive = false; + } + + handleChangeComplete(); + } + + @Override + public void onCreate() { + HandlerThread thread = new HandlerThread("ChangeEncryptionPasswordService", HandlerThread.NORM_PRIORITY); + thread.start(); + + serviceLooper = thread.getLooper(); + serviceHandler = new ServiceHandler(serviceLooper); + + notifyManager = (NotificationManager)getBaseContext().getSystemService(Context.NOTIFICATION_SERVICE); + notificationBuilder = new NotificationCompat.Builder(getBaseContext()); + } + + private final class ServiceHandler extends Handler { + + public ServiceHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + Log.d(TAG, "handleMessage()"); + Intent intent = msg.getData().getParcelable(KEY_INTENT); + + if (intent != null) { + if (intent.getExtras() != null && + intent.getExtras().get(KEY_MESSENGER) != null && + intent.getExtras().getString(KEY_OLD_MASTER_PASSPHRASE) != null && + intent.getExtras().getString(KEY_NEW_MASTER_PASSPHRASE) != null && + intent.getExtras().getBundle(KEY_ACCOUNT) != null) + { + if (!DavAccount.build(intent.getExtras().getBundle(KEY_ACCOUNT)).isPresent()) { + Log.e(TAG, "received bad account bundle"); + return; + } + + messenger = (Messenger) intent.getExtras().get(KEY_MESSENGER); + oldMasterPassphrase = intent.getExtras().getString(KEY_OLD_MASTER_PASSPHRASE); + newMasterPassphrase = intent.getExtras().getString(KEY_NEW_MASTER_PASSPHRASE); + account = DavAccount.build(intent.getExtras().getBundle(KEY_ACCOUNT)).get(); + + handleStartChangeEncryptionPassword(); + } + else + Log.e(TAG, "received intent without messenger, old or new master passphrase, or account"); + } + else + Log.e(TAG, "received message with null intent"); + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Message msg = serviceHandler.obtainMessage(); + msg.getData().putParcelable(KEY_INTENT, intent); + serviceHandler.sendMessage(msg); + + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + Log.d(TAG, "onBind()"); + return null; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/ContactCopyService.java b/flock/src/main/java/org/anhonesteffort/flock/ContactCopyService.java new file mode 100644 index 0000000..8285597 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/ContactCopyService.java @@ -0,0 +1,273 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.accounts.Account; +import android.app.NotificationManager; +import android.app.Service; +import android.content.ContentProviderClient; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.support.v4.app.NotificationCompat; +import android.util.Log; +import android.util.Pair; + +import org.anhonesteffort.flock.sync.addressbook.AddressbookSyncScheduler; +import org.anhonesteffort.flock.sync.addressbook.ContactCopiedListener; +import org.anhonesteffort.flock.sync.addressbook.LocalContactCollection; +import org.anhonesteffort.flock.webdav.InvalidComponentException; + +import java.util.LinkedList; +import java.util.List; + +/** + * Programmer: rhodey + */ +public class ContactCopyService extends Service implements ContactCopiedListener { + + private static final String TAG = "org.anhonesteffort.flock.ContactCopyService"; + + protected static final String ACTION_QUEUE_ACCOUNT_FOR_COPY = "ContactCopyService.ACTION_QUEUE_ACCOUNT_FOR_COPY"; + protected static final String ACTION_START_COPY = "ContactCopyService.ACTION_START_COPY"; + + private static final String KEY_INTENT = "ContactCopyService.KEY_INTENT"; + private static final String KEY_ACCOUNT_WITH_ERROR = "ContactCopyService.KEY_ACCOUNT_WITH_ERROR"; + private static final String KEY_ACCOUNT_ERROR_COUNT = "ContactCopyService.KEY_ACCOUNT_ERROR_COUNT"; + + protected static final String KEY_FROM_ACCOUNT = "ContactCopyService.KEY_FROM_ACCOUNT"; + protected static final String KEY_TO_ACCOUNT = "ContactCopyService.KEY_TO_ACCOUNT"; + protected static final String KEY_CONTACT_COUNT = "ContactCopyService.KEY_CONTACT_COUNT"; + + private final List> accountsForCopy = new LinkedList>(); + + private Looper serviceLooper; + private ServiceHandler serviceHandler; + private NotificationManager notifyManager; + private NotificationCompat.Builder notificationBuilder; + private List accountErrors; + + private int countContactsToCopy = 0; + private int countContactsCopied = 0; + private int countContactCopiesFailed = 0; + + private void handleInitializeNotification() { + Log.d(TAG, "handleInitializeNotification()"); + + notificationBuilder.setContentTitle(getString(R.string.notification_contact_import)) + .setContentText(getString(R.string.notification_importing_contacts)) + .setProgress(100, 1, false) + .setSmallIcon(R.drawable.flock_actionbar_icon); + + startForeground(1023, notificationBuilder.build()); + } + + private void handleContactCopied(Account fromAccount) { + countContactsCopied++; + Log.d(TAG, "handleContactCopied() contacts copied: " + countContactsCopied); + + notificationBuilder + .setContentText(getString(R.string.notification_importing_contacts_from) + + " " + fromAccount.name) + .setProgress(countContactsToCopy, countContactsCopied + countContactCopiesFailed, false); + + notifyManager.notify(1023, notificationBuilder.build()); + } + + private void handleContactCopyFailed(Account fromAccount) { + countContactCopiesFailed++; + Log.d(TAG, "handleContactCopyFailed() contact copies failed: " + countContactCopiesFailed); + + boolean accountWasFound = false; + for (Bundle accountError : accountErrors) { + Account accountWithError = accountError.getParcelable(KEY_ACCOUNT_WITH_ERROR); + Integer errorCount = accountError.getInt(KEY_ACCOUNT_ERROR_COUNT, -1); + + if (accountWithError != null && errorCount > 0) { + if (fromAccount.equals(accountWithError)) { + accountError.putInt(KEY_ACCOUNT_ERROR_COUNT, errorCount + 1); + accountWasFound = true; + break; + } + } + } + + if (!accountWasFound) { + Bundle errorBundle = new Bundle(); + errorBundle.putParcelable(KEY_ACCOUNT_WITH_ERROR, fromAccount); + errorBundle.putInt(KEY_ACCOUNT_ERROR_COUNT, 1); + + accountErrors.add(errorBundle); + } + + notificationBuilder + .setContentText(getString(R.string.notification_importing_contacts_from) + + " " + fromAccount.name) + .setProgress(countContactsToCopy, countContactsCopied + countContactCopiesFailed, false); + + notifyManager.notify(1023, notificationBuilder.build()); + } + + private void handleCopyComplete() { + Log.d(TAG, "handleCopyComplete()"); + + new AddressbookSyncScheduler(getBaseContext()).requestSync(); + stopForeground(false); + stopSelf(); + } + + @Override + public void onDestroy() { + Log.d(TAG, "onDestroy()"); + + if (countContactCopiesFailed == 0) { + notificationBuilder + .setProgress(0, 0, false) + .setContentText(getString(R.string.notification_import_complete_copied) + + " " + countContactsCopied + " " + getString(R.string.contacts) + + getString(R.string.period)); + } + else { + notificationBuilder + .setProgress(0, 0, false) + .setContentText(getString(R.string.notification_import_complete_copied) + + " " + countContactsCopied + " " + getString(R.string.contacts) + ", " + + countContactCopiesFailed + " " + + getString(R.string.failed) + getString(R.string.period)); + } + + notifyManager.notify(1023, notificationBuilder.build()); + } + + private void handleQueueAccountForCopy(Intent intent) { + Log.d(TAG, "handleQueueAccountForCopy()"); + + Account fromAccount = intent.getParcelableExtra(KEY_FROM_ACCOUNT); + Account toAccount = intent.getParcelableExtra(KEY_TO_ACCOUNT); + Integer contactCount = intent.getIntExtra(KEY_CONTACT_COUNT, -1); + + if (fromAccount == null || toAccount == null || contactCount < 0) { + Log.e(TAG, "failed to parse to account, from account, or contact count from intent extras."); + return; + } + + accountsForCopy.add(new Pair(fromAccount, toAccount)); + countContactsToCopy += contactCount; + + Log.d(TAG, "contacts to copy: " + countContactsToCopy); + } + + private void handleStartCopy() { + Log.d(TAG, "handleStartCopy()"); + handleInitializeNotification(); + + ContentProviderClient client = getBaseContext().getContentResolver() + .acquireContentProviderClient(AddressbookSyncScheduler.CONTENT_AUTHORITY); + + for (Pair copyPair : accountsForCopy) { + LocalContactCollection fromCollection = + new LocalContactCollection(getBaseContext(), client, copyPair.first, "hack"); + + try { + + fromCollection.copyToAccount(copyPair.second, this); + + } catch (InvalidComponentException e) { + ErrorToaster.handleShowError(getBaseContext(), e); + return; + } catch (RemoteException e) { + ErrorToaster.handleShowError(getBaseContext(), e); + return; + } + } + + handleCopyComplete(); + } + + @Override + public void onContactCopied(Account fromAccount, Account toAccount) { + handleContactCopied(fromAccount); + } + + @Override + public void onContactCopyFailed(Exception e, Account fromAccount, Account toAccount) { + Log.e(TAG, "onContactCopyFailed() from " + fromAccount.name + " to " + toAccount.name, e); + handleContactCopyFailed(fromAccount); + } + + @Override + public void onCreate() { + HandlerThread thread = new HandlerThread("ContactCopyService", HandlerThread.NORM_PRIORITY); + thread.start(); + + serviceLooper = thread.getLooper(); + serviceHandler = new ServiceHandler(serviceLooper); + + notifyManager = (NotificationManager)getBaseContext().getSystemService(Context.NOTIFICATION_SERVICE); + notificationBuilder = new NotificationCompat.Builder(getBaseContext()); + accountErrors = new LinkedList(); + } + + private final class ServiceHandler extends Handler { + + public ServiceHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + Log.d(TAG, "handleMessage()"); + Intent intent = msg.getData().getParcelable(KEY_INTENT); + + if (intent != null && intent.getAction() != null) { + if (intent.getAction().equals(ACTION_QUEUE_ACCOUNT_FOR_COPY)) + handleQueueAccountForCopy(intent); + else if (intent.getAction().equals(ACTION_START_COPY)) + handleStartCopy(); + else + Log.e(TAG, "received intent with unknown action"); + } + else + Log.e(TAG, "received message with null intent or intent action"); + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Message msg = serviceHandler.obtainMessage(); + msg.getData().putParcelable(KEY_INTENT, intent); + serviceHandler.sendMessage(msg); + + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + Log.d(TAG, "onBind()"); + return null; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/CorrectEncryptionPasswordActivity.java b/flock/src/main/java/org/anhonesteffort/flock/CorrectEncryptionPasswordActivity.java new file mode 100644 index 0000000..53044ed --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/CorrectEncryptionPasswordActivity.java @@ -0,0 +1,145 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.os.AsyncTask; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.view.Window; +import android.widget.TextView; +import android.widget.Toast; + +import org.anhonesteffort.flock.crypto.KeyHelper; +import org.anhonesteffort.flock.crypto.KeyStore; +import org.anhonesteffort.flock.sync.key.KeySyncScheduler; +import org.anhonesteffort.flock.sync.key.KeySyncUtil; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +/** + * Programmer: rhodey + */ +public class CorrectEncryptionPasswordActivity extends Activity { + + private static final String TAG = "org.anhonesteffort.flock.CorrectEncryptionPasswordActivity"; + + private AsyncTask asyncTask; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + requestWindowFeature(Window.FEATURE_PROGRESS); + + setContentView(R.layout.correct_encryption_password); + getActionBar().setTitle(R.string.title_correct_encryption_password); + + findViewById(R.id.test_master_passphrase_button).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + handleTestMasterPassphrase(); + } + }); + } + + @Override + public void onPause() { + super.onPause(); + + if (asyncTask != null && !asyncTask.isCancelled()) + asyncTask.cancel(true); + } + + private void handleEncryptionPasswordValid() { + Log.d(TAG, "handleEncryptionPasswordValid()"); + + Toast.makeText(getBaseContext(), + R.string.password_corrected, + Toast.LENGTH_SHORT).show(); + + new KeySyncScheduler(getBaseContext()).requestSync(); + finish(); + } + + private void handleTestMasterPassphrase() { + asyncTask = new AsyncTask() { + + @Override + protected void onPreExecute() { + Log.d(TAG, "handleTestMasterPassphrase()"); + setProgressBarIndeterminateVisibility(true); + setProgressBarVisibility(true); + } + + @Override + protected Bundle doInBackground(String... params) { + Bundle result = new Bundle(); + String masterPassphrase = ((TextView)findViewById(R.id.cipher_passphrase)).getText().toString().trim(); + + if (TextUtils.isEmpty(masterPassphrase)) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_INVALID_CIPHER_PASSPHRASE); + return result; + } + + KeyStore.saveMasterPassphrase(getBaseContext(), masterPassphrase); + + try { + + if (KeyHelper.masterPassphraseIsValid(getBaseContext())) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + KeySyncUtil.cancelCipherPassphraseNotification(getBaseContext()); + } + else + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_INVALID_CIPHER_PASSPHRASE); + + } catch (GeneralSecurityException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + Log.e(TAG, "doInBackground()", e); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_CRYPTO_ERROR); + } + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + Log.d(TAG, "STATUS: " + result.getInt(ErrorToaster.KEY_STATUS_CODE)); + setProgressBarIndeterminateVisibility(false); + setProgressBarVisibility(false); + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) + handleEncryptionPasswordValid(); + else + ErrorToaster.handleDisplayToastBundledError(getBaseContext(), result); + + ((TextView) findViewById(R.id.cipher_passphrase)).setText(""); + } + + }.execute(); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/CorrectPasswordActivity.java b/flock/src/main/java/org/anhonesteffort/flock/CorrectPasswordActivity.java new file mode 100644 index 0000000..9a95c43 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/CorrectPasswordActivity.java @@ -0,0 +1,170 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.text.TextUtils; +import android.util.Log; +import android.view.MenuItem; +import android.view.View; +import android.view.Window; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.sync.addressbook.AddressbookSyncScheduler; +import org.anhonesteffort.flock.sync.calendar.CalendarsSyncScheduler; +import org.anhonesteffort.flock.sync.key.KeySyncScheduler; + +/** + * Programmer: rhodey + */ +public class CorrectPasswordActivity extends Activity { + + private static final String TAG = "org.anhonesteffort.flock.CorrectPasswordActivity"; + + private String accountUsername; + private String accountDavHREF; + private boolean serviceStarted = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + requestWindowFeature(Window.FEATURE_PROGRESS); + + setContentView(R.layout.correct_password); + getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setTitle(R.string.title_correct_sync_password); + + getUsernameAndHrefOrFinish(); + + findViewById(R.id.login_button).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + handleStartCorrectPasswordService(); + } + }); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + break; + } + + return false; + } + + private void getUsernameAndHrefOrFinish() { + Optional username = DavAccountHelper.getAccountUsername(getBaseContext()); + Optional davHref = DavAccountHelper.getAccountDavHREF(getBaseContext()); + + if (!DavAccountHelper.isAccountRegistered(getBaseContext()) || + !username.isPresent() || + !davHref.isPresent()) + { + Log.e(TAG, "not account is registered, not sure how we got here"); + Toast.makeText(this, R.string.error_no_account_is_registered, Toast.LENGTH_SHORT).show(); + startActivity(new Intent(getBaseContext(), SetupActivity.class)); + finish(); + return; + } + + accountUsername = username.get(); + accountDavHREF = davHref.get(); + } + + private void handleLoginSuccess() { + Log.d(TAG, "handleLoginSuccess"); + Toast.makeText(getBaseContext(), + R.string.login_successful, + Toast.LENGTH_SHORT).show(); + + new KeySyncScheduler(getBaseContext()).requestSync(); + new CalendarsSyncScheduler(getBaseContext()).requestSync(); + new AddressbookSyncScheduler(getBaseContext()).requestSync(); + + finish(); + } + + private void handleStartCorrectPasswordService() { + Log.d(TAG, "handleStartCorrectPasswordService()"); + + if (serviceStarted) + return; + + String password = ((TextView)findViewById(R.id.account_password)).getText().toString().trim(); + DavAccount account = new DavAccount(accountUsername, password, accountDavHREF); + + if (TextUtils.isEmpty(password)) { + Toast.makeText(getBaseContext(), + R.string.error_password_too_short, + Toast.LENGTH_SHORT).show(); + return; + } + + Intent correctService = new Intent(getBaseContext(), CorrectPasswordService.class); + + correctService.putExtra(CorrectPasswordService.KEY_MESSENGER, new Messenger(new MessageHandler())); + correctService.putExtra(CorrectPasswordService.KEY_MASTER_PASSPHRASE, password); + correctService.putExtra(CorrectPasswordService.KEY_ACCOUNT, account.toBundle()); + + startService(correctService); + serviceStarted = true; + + setProgressBarIndeterminateVisibility(true); + setProgressBarVisibility(true); + } + + public class MessageHandler extends Handler { + + @Override + public void handleMessage(Message message) { + setProgressBarIndeterminateVisibility(false); + setProgressBarVisibility(false); + + if (message.arg1 == ErrorToaster.CODE_SUCCESS) + handleLoginSuccess(); + + else { + serviceStarted = false; + Bundle errorBundler = new Bundle(); + + errorBundler.putInt(ErrorToaster.KEY_STATUS_CODE, message.arg1); + ErrorToaster.handleDisplayToastBundledError(getBaseContext(), errorBundler); + + if (findViewById(R.id.account_password) != null) + ((TextView)findViewById(R.id.account_password)).setText(""); + } + } + + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/CorrectPasswordService.java b/flock/src/main/java/org/anhonesteffort/flock/CorrectPasswordService.java new file mode 100644 index 0000000..f288ecd --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/CorrectPasswordService.java @@ -0,0 +1,252 @@ +package org.anhonesteffort.flock; + +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.support.v4.app.NotificationCompat; +import android.util.Log; + +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.crypto.KeyHelper; +import org.anhonesteffort.flock.crypto.KeyStore; +import org.anhonesteffort.flock.crypto.KeyUtil; +import org.anhonesteffort.flock.sync.AbstractDavSyncAdapter; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.jackrabbit.webdav.DavException; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +import javax.net.ssl.SSLException; + +/** + * Created by rhodey. + */ +public class CorrectPasswordService extends Service { + + private static final String TAG = "org.anhonesteffort.flock.CorrectPasswordService"; + + private static final String KEY_INTENT = "CorrectPasswordService.KEY_INTENT"; + protected static final String KEY_MESSENGER = "CorrectPasswordService.KEY_MESSENGER"; + protected static final String KEY_MASTER_PASSPHRASE = "CorrectPasswordService.KEY_OLD_MASTER_PASSPHRASE"; + protected static final String KEY_ACCOUNT = "CorrectPasswordService.KEY_ACCOUNT"; + + private Looper serviceLooper; + private ServiceHandler serviceHandler; + private NotificationManager notifyManager; + private NotificationCompat.Builder notificationBuilder; + + private Messenger messenger; + private String masterPassphrase; + private DavAccount account; + + private int resultCode; + private boolean remoteActivityIsAlive = true; + + private void handleInitializeNotification() { + Log.d(TAG, "handleInitializeNotification()"); + + notificationBuilder.setContentTitle(getString(R.string.title_correct_sync_password)) + .setContentText(getString(R.string.updating_encryption_secrets)) + .setProgress(0, 0, true) + .setSmallIcon(R.drawable.flock_actionbar_icon); + + startForeground(9001, notificationBuilder.build()); + } + + @Override + public void onDestroy() { + Log.d(TAG, "onDestroy()"); + + if (remoteActivityIsAlive || resultCode == ErrorToaster.CODE_SUCCESS) + return; + + Bundle errorBundler = new Bundle(); + errorBundler.putInt(ErrorToaster.KEY_STATUS_CODE, resultCode); + ErrorToaster.handleDisplayToastBundledError(getBaseContext(), errorBundler); + + notificationBuilder + .setProgress(0, 0, false) + .setContentText(getString(R.string.error_login_unauthorized)); + + notifyManager.notify(9001, notificationBuilder.build()); + } + + + private void handleCorrectComplete() { + Log.d(TAG, "handleCorrectComplete()"); + + if (remoteActivityIsAlive || resultCode == ErrorToaster.CODE_SUCCESS) + stopForeground(true); + else + stopForeground(false); + + stopSelf(); + } + + private void handleUpdateMasterPassphrase(Bundle result, String masterPassphrase) { + Log.d(TAG, "handleUpdateMasterPassphrase()"); + KeyStore.saveMasterPassphrase(getBaseContext(), masterPassphrase); + + try { + + Optional encryptedKeyMaterial = KeyHelper.buildEncryptedKeyMaterial(getBaseContext()); + if (encryptedKeyMaterial.isPresent()) { + KeyStore.saveEncryptedKeyMaterial(getBaseContext(), encryptedKeyMaterial.get()); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + } + + else { + Log.e(TAG, "unable to build encrypted key material"); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_CRYPTO_ERROR); + } + + } catch (GeneralSecurityException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + Log.e(TAG, "caught exception while updating master passphrase", e); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_CRYPTO_ERROR); + } + } + + private void handleDavLogin(Bundle result, DavAccount account) { + Log.d(TAG, "handleDavLogin()"); + + if (DavAccountHelper.isUsingOurServers(account)) { + try { + + String owsAuthToken = KeyUtil.getAuthTokenForPassphrase(masterPassphrase); + account = new DavAccount(account.getUserId(), owsAuthToken, account.getDavHostHREF()); + + } catch (GeneralSecurityException e) { + ErrorToaster.handleBundleError(e, result); + return; + } + } + + try { + + if (DavAccountHelper.isAuthenticated(getBaseContext(), account)) { + DavAccountHelper.setAccountPassword(getBaseContext(), account.getAuthToken()); + AbstractDavSyncAdapter.cancelAuthNotification(getBaseContext()); + + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + } + else + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_UNAUTHORIZED); + + } catch (PropertyParseException e) { + ErrorToaster.handleBundleError(e, result); + } catch (DavException e) { + ErrorToaster.handleBundleError(e, result); + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + } + + private void handleStartCorrectPassword() { + Log.d(TAG, "handleStartCorrectPassword()"); + + Bundle result = new Bundle(); + + handleInitializeNotification(); + handleDavLogin(result, account); + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + if (DavAccountHelper.isUsingOurServers(account)) + handleUpdateMasterPassphrase(result, masterPassphrase); + } + + Message message = Message.obtain(); + message.arg1 = result.getInt(ErrorToaster.KEY_STATUS_CODE); + resultCode = result.getInt(ErrorToaster.KEY_STATUS_CODE); + + try { + + messenger.send(message); + + } catch (RemoteException e) { + Log.e(TAG, "caught exception while sending message to activity >> ", e); + remoteActivityIsAlive = false; + } + + handleCorrectComplete(); + } + + @Override + public void onCreate() { + HandlerThread thread = new HandlerThread("CorrectPasswordService", HandlerThread.NORM_PRIORITY); + thread.start(); + + serviceLooper = thread.getLooper(); + serviceHandler = new ServiceHandler(serviceLooper); + + notifyManager = (NotificationManager)getBaseContext().getSystemService(Context.NOTIFICATION_SERVICE); + notificationBuilder = new NotificationCompat.Builder(getBaseContext()); + } + + private final class ServiceHandler extends Handler { + + public ServiceHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + Log.d(TAG, "handleMessage()"); + Intent intent = msg.getData().getParcelable(KEY_INTENT); + + if (intent != null) { + if (intent.getExtras() != null && + intent.getExtras().get(KEY_MESSENGER) != null && + intent.getExtras().getString(KEY_MASTER_PASSPHRASE) != null && + intent.getExtras().getBundle(KEY_ACCOUNT) != null) + { + if (!DavAccount.build(intent.getExtras().getBundle(KEY_ACCOUNT)).isPresent()) { + Log.e(TAG, "received bad account bundle"); + return; + } + + messenger = (Messenger) intent.getExtras().get(KEY_MESSENGER); + masterPassphrase = intent.getExtras().getString(KEY_MASTER_PASSPHRASE); + account = DavAccount.build(intent.getExtras().getBundle(KEY_ACCOUNT)).get(); + + handleStartCorrectPassword(); + } + else + Log.e(TAG, "received intent without messenger, master passphrase or account"); + } + else + Log.e(TAG, "received message with null intent"); + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Message msg = serviceHandler.obtainMessage(); + msg.getData().putParcelable(KEY_INTENT, intent); + serviceHandler.sendMessage(msg); + + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + Log.d(TAG, "onBind()"); + return null; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/DavAccountHelper.java b/flock/src/main/java/org/anhonesteffort/flock/DavAccountHelper.java new file mode 100644 index 0000000..df30cbb --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/DavAccountHelper.java @@ -0,0 +1,255 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.accounts.AccountManager; +import android.content.Context; +import android.content.SharedPreferences; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.crypto.MasterCipher; +import org.anhonesteffort.flock.sync.AndroidDavClient; +import org.anhonesteffort.flock.sync.OwsWebDav; +import org.anhonesteffort.flock.sync.addressbook.HidingCardDavStore; +import org.anhonesteffort.flock.sync.calendar.HidingCalDavStore; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.anhonesteffort.flock.webdav.caldav.CalDavStore; +import org.anhonesteffort.flock.webdav.carddav.CardDavStore; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; + +/** + * Programmer: rhodey + */ +// TODO: this is getting to be a mess :( +public class DavAccountHelper { + + private static final String PREFERENCES_NAME = "org.anhonesteffort.flock.DavAccountHelper"; + private static final String KEY_DAV_USERNAME = "org.anhonesteffort.flock.auth.AccountAuthenticator.KEY_DAV_USERNAME"; + private static final String KEY_DAV_PASSWORD = "org.anhonesteffort.flock.auth.AccountAuthenticator.KEY_DAV_PASSWORD"; + private static final String KEY_DAV_HOST = "org.anhonesteffort.flock.auth.AccountAuthenticator.KEY_DAV_HOST"; + + private static SharedPreferences getSharedPreferences(Context context) { + return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_MULTI_PROCESS); + } + + public static String correctUsername(Context context, String username) { + if (!username.contains("@")) + return username.concat(context.getString(R.string.at_flock_sync)); + + return username; + } + + public static boolean isAccountRegistered(Context context) { + return AccountManager.get(context).getAccountsByType(DavAccount.SYNC_ACCOUNT_TYPE).length > 0; + } + + public static Optional getAccountUsername(Context context) { + return Optional.fromNullable(getSharedPreferences(context).getString(KEY_DAV_USERNAME, null)); + } + + public static void setAccountUsername(Context context, String username) { + getSharedPreferences(context).edit().putString(KEY_DAV_USERNAME, username).commit(); + } + + public static Optional getAccountPassword(Context context) { + return Optional.fromNullable(getSharedPreferences(context).getString(KEY_DAV_PASSWORD, null)); + } + + public static void setAccountPassword(Context context, String password) { + getSharedPreferences(context).edit().putString(KEY_DAV_PASSWORD, password).commit(); + } + + public static void invalidateAccountPassword(Context context) { + getSharedPreferences(context).edit().remove(KEY_DAV_PASSWORD).commit(); + } + + public static Optional getAccountDavHREF(Context context) { + return Optional.fromNullable(getSharedPreferences(context).getString(KEY_DAV_HOST, null)); + } + + public static void setAccountDavHREF(Context context, String username) { + getSharedPreferences(context).edit().putString(KEY_DAV_HOST, username).commit(); + } + + public static void invalidateAccount(Context context) { + getSharedPreferences(context).edit().remove(KEY_DAV_HOST).commit(); + getSharedPreferences(context).edit().remove(KEY_DAV_USERNAME).commit(); + getSharedPreferences(context).edit().remove(KEY_DAV_PASSWORD).commit(); + } + + public static boolean isUsingOurServers(DavAccount account) { + return account.getDavHostHREF().equals(OwsWebDav.HREF_WEBDAV_HOST); + } + + public static boolean isUsingOurServers(Context context) { + return getAccountDavHREF(context).isPresent() && + getAccountDavHREF(context).get().equals(OwsWebDav.HREF_WEBDAV_HOST); + } + + public static Optional getAccount(Context context) { + Optional davHREF = getAccountDavHREF(context); + Optional accountUsername = getAccountUsername(context); + Optional accountPassword = getAccountPassword(context); + + if (!isAccountRegistered(context) || !davHREF.isPresent() || + !accountUsername.isPresent() || !accountPassword.isPresent()) + { + return Optional.absent(); + } + + return Optional.of(new DavAccount(accountUsername.get(), accountPassword.get(), davHREF.get())); + } + + private static AndroidDavClient getAndroidDavClient(Context context, DavAccount account) + throws MalformedURLException + { + URL davHost = new URL(account.getDavHostHREF()); + + return new AndroidDavClient(context, + davHost, + account.getUserId(), + account.getAuthToken()); + } + + private static String getOwsCurrentUserPrincipal(DavAccount account) throws IOException { + return "/principals/__uids__/" + URLEncoder.encode(account.getUserId().toUpperCase(), "UTF8") + "/"; + } + + private static String getOwsCalendarHomeSet(DavAccount account) throws IOException { + return "/calendars/__uids__/" + URLEncoder.encode(account.getUserId().toUpperCase(), "UTF8") + "/"; + } + + private static String getOwsAddressbookHomeSet(DavAccount account) throws IOException { + return "/addressbooks/__uids__/" + URLEncoder.encode(account.getUserId().toUpperCase(), "UTF8") + "/"; + } + + public static CardDavStore getCardDavStore(Context context, DavAccount account) + throws DavException, IOException + { + if (isUsingOurServers(account)) + return new CardDavStore(getAndroidDavClient(context, account), + Optional.of(getOwsCurrentUserPrincipal(account)), + Optional.of(getOwsAddressbookHomeSet(account))); + + return new CardDavStore(getAndroidDavClient(context, account), + Optional.absent(), + Optional.absent()); + } + + public static CalDavStore getCalDavStore(Context context, DavAccount account) + throws DavException, IOException + { + if (isUsingOurServers(account)) + return new CalDavStore(getAndroidDavClient(context, account), + Optional.of(getOwsCurrentUserPrincipal(account)), + Optional.of(getOwsCalendarHomeSet(account))); + + return new CalDavStore(getAndroidDavClient(context, account), + Optional.absent(), + Optional.absent()); + } + + public static HidingCardDavStore getHidingCardDavStore(Context context, + DavAccount account, + MasterCipher masterCipher) + throws DavException, IOException + { + if (isUsingOurServers(account)) + return new HidingCardDavStore(masterCipher, + getAndroidDavClient(context, account), + Optional.of(getOwsCurrentUserPrincipal(account)), + Optional.of(getOwsAddressbookHomeSet(account))); + + return new HidingCardDavStore(masterCipher, + getAndroidDavClient(context, account), + Optional.absent(), + Optional.absent()); + } + + public static HidingCalDavStore getHidingCalDavStore(Context context, + DavAccount account, + MasterCipher masterCipher) + throws DavException, IOException + { + if (isUsingOurServers(account)) + return new HidingCalDavStore(masterCipher, + getAndroidDavClient(context, account), + Optional.of(getOwsCurrentUserPrincipal(account)), + Optional.of(getOwsCalendarHomeSet(account))); + + return new HidingCalDavStore(masterCipher, + getAndroidDavClient(context, account), + Optional.absent(), + Optional.absent()); + } + + public static boolean isAuthenticated(Context context, DavAccount account) + throws PropertyParseException, DavException, IOException + { + CardDavStore cardDavStore = getCardDavStore(context, account); + + try { + + cardDavStore.getCollections(); + return true; + + } catch (DavException e) { + + if (e.getErrorCode() == OwsWebDav.STATUS_PAYMENT_REQUIRED) + return true; + else if (e.getErrorCode() == DavServletResponse.SC_UNAUTHORIZED) + return false; + else + throw e; + + } finally { + cardDavStore.closeHttpConnection(); + } + } + + public static boolean isExpired(Context context, DavAccount account) + throws PropertyParseException, DavException, IOException + { + CardDavStore cardDavStore = getCardDavStore(context, account); + + try { + + cardDavStore.getCollections(); + return false; + + } catch (DavException e) { + + if (e.getErrorCode() == OwsWebDav.STATUS_PAYMENT_REQUIRED) + return true; + else + throw e; + + } finally { + cardDavStore.closeHttpConnection(); + } + } +} \ No newline at end of file diff --git a/flock/src/main/java/org/anhonesteffort/flock/EditAutoRenewActivity.java b/flock/src/main/java/org/anhonesteffort/flock/EditAutoRenewActivity.java new file mode 100644 index 0000000..9d04eb5 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/EditAutoRenewActivity.java @@ -0,0 +1,750 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.app.Activity; +import android.os.AsyncTask; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.Window; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.common.base.Optional; +import com.stripe.exception.CardException; +import com.stripe.exception.StripeException; +import com.stripe.model.Token; + +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.registration.OwsRegistration; +import org.anhonesteffort.flock.registration.RegistrationApi; +import org.anhonesteffort.flock.registration.RegistrationApiException; +import org.anhonesteffort.flock.registration.model.AugmentedFlockAccount; +import org.anhonesteffort.flock.registration.model.FlockAccount; +import org.anhonesteffort.flock.registration.model.FlockCardInformation; +import org.apache.commons.lang.StringUtils; + +import java.io.IOException; +import java.util.HashMap; + +/** + * Programmer: rhodey + */ +public class EditAutoRenewActivity extends Activity { + + private static final String TAG = "org.anhonesteffort.flock.EditAutoRenewActivity"; + + public static final String KEY_DAV_ACCOUNT_BUNDLE = "KEY_DAV_ACCOUNT_BUNDLE"; + public static final String KEY_FLOCK_ACCOUNT_BUNDLE = "KEY_FLOCK_ACCOUNT_BUNDLE"; + public static final String KEY_CARD_INFORMATION_BUNDLE = "KEY_CARD_INFORMATION_BUNDLE"; + + private DavAccount davAccount; + private AsyncTask asyncTask; + private TextWatcher cardNumberTextWatcher; + private TextWatcher cardExpirationTextWatcher; + + private Optional flockAccount = Optional.absent(); + private Optional cardInformation = Optional.absent(); + + private int lastCardExpirationLength = 0; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + requestWindowFeature(Window.FEATURE_PROGRESS); + + setContentView(R.layout.activity_edit_auto_renew); + getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setTitle(R.string.button_edit_payment_details); + + if (savedInstanceState != null && !savedInstanceState.isEmpty()) { + if (!DavAccount.build(savedInstanceState.getBundle(KEY_DAV_ACCOUNT_BUNDLE)).isPresent()) { + finish(); + return; + } + + davAccount = DavAccount.build(savedInstanceState.getBundle(KEY_DAV_ACCOUNT_BUNDLE)).get(); + flockAccount = FlockAccount.build(savedInstanceState.getBundle(KEY_FLOCK_ACCOUNT_BUNDLE)); + cardInformation = FlockCardInformation.build(savedInstanceState.getBundle(KEY_CARD_INFORMATION_BUNDLE)); + } + else if (getIntent().getExtras() != null) { + if (!DavAccount.build(getIntent().getExtras().getBundle(KEY_DAV_ACCOUNT_BUNDLE)).isPresent()) { + finish(); + return; + } + + davAccount = DavAccount.build(getIntent().getExtras().getBundle(KEY_DAV_ACCOUNT_BUNDLE)).get(); + flockAccount = FlockAccount.build(getIntent().getExtras().getBundle(KEY_FLOCK_ACCOUNT_BUNDLE)); + cardInformation = FlockCardInformation.build(getIntent().getExtras().getBundle(KEY_CARD_INFORMATION_BUNDLE)); + } + + initCostPerYear(); + } + + private void initCostPerYear() { + TextView costPerYearView = (TextView) findViewById(R.id.cost_per_year); + double costPerYearUsd = (double) getResources().getInteger(R.integer.cost_per_year_usd); + + costPerYearView.setText(getString(R.string.usd_per_year, costPerYearUsd)); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + break; + } + + return false; + } + + @Override + public void onResume() { + super.onResume(); + + ((CheckBox) findViewById(R.id.checkbox_enable_auto_renew)).setChecked(false); + handleDisableForm(); + + handleGetAccountAndCardAsync(); + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + savedInstanceState.putBundle(KEY_DAV_ACCOUNT_BUNDLE, davAccount.toBundle()); + + if (flockAccount.isPresent()) + savedInstanceState.putBundle(KEY_FLOCK_ACCOUNT_BUNDLE, flockAccount.get().toBundle()); + + if (cardInformation.isPresent()) + savedInstanceState.putBundle(KEY_CARD_INFORMATION_BUNDLE, cardInformation.get().toBundle()); + + super.onSaveInstanceState(savedInstanceState); + } + + @Override + public void onPause() { + super.onPause(); + + if (asyncTask != null && !asyncTask.isCancelled()) + asyncTask.cancel(true); + + ((EditText) findViewById(R.id.card_number)).setText(""); + ((EditText) findViewById(R.id.card_expiration)).setText(""); + } + + private void handleDisableForm() { + Log.d(TAG, "handleDisableForm()"); + + Button verifyAndSaveButton = (Button) findViewById(R.id.button_verify_and_save); + EditText cardExpirationView = (EditText) findViewById(R.id.card_expiration); + EditText cardNumberView = (EditText) findViewById(R.id.card_number); + TextView cardCVCView = (TextView) findViewById(R.id.card_cvc); + + cardNumberView.setFocusable(false); + cardNumberView.setFocusableInTouchMode(false); + cardExpirationView.setFocusable(false); + cardExpirationView.setFocusableInTouchMode(false); + cardCVCView.setFocusable(false); + cardCVCView.setFocusableInTouchMode(false); + + cardNumberView.setText(""); + cardExpirationView.setText(""); + cardCVCView.setText(""); + + cardNumberView.setOnTouchListener(null); + cardExpirationView.setOnTouchListener(null); + cardCVCView.setOnTouchListener(null); + + verifyAndSaveButton.setText(R.string.button_save); + verifyAndSaveButton.setOnClickListener(null); + } + + private void handleInitFormAsAutoRenewDisabled() { + Log.d(TAG, "handleInitFormFormAsAutoRenewDisabled()"); + + Button verifyAndSaveButton = (Button) findViewById(R.id.button_verify_and_save); + EditText cardExpirationView = (EditText) findViewById(R.id.card_expiration); + EditText cardNumberView = (EditText) findViewById(R.id.card_number); + TextView cardCVCView = (TextView) findViewById(R.id.card_cvc); + + cardNumberView.setFocusable(false); + cardNumberView.setFocusableInTouchMode(false); + cardExpirationView.setFocusable(false); + cardExpirationView.setFocusableInTouchMode(false); + cardCVCView.setFocusable(false); + cardCVCView.setFocusableInTouchMode(false); + + cardNumberView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) + ((CheckBox) findViewById(R.id.checkbox_enable_auto_renew)).setChecked(true); + + return false; + } + }); + cardExpirationView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) + ((CheckBox) findViewById(R.id.checkbox_enable_auto_renew)).setChecked(true); + + return false; + } + }); + cardCVCView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) + ((CheckBox) findViewById(R.id.checkbox_enable_auto_renew)).setChecked(true); + + return false; + } + }); + + cardNumberView.setText(""); + cardExpirationView.setText(""); + cardCVCView.setText(""); + + verifyAndSaveButton.setText(R.string.button_save); + verifyAndSaveButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + handleSaveAutoRenewAndFinish(false); + } + }); + } + + private void handleEnableForm() { + Log.d(TAG, "handleEnableForm()"); + + EditText cardExpirationView = (EditText) findViewById(R.id.card_expiration); + EditText cardNumberView = (EditText) findViewById(R.id.card_number); + TextView cardCVCView = (TextView) findViewById(R.id.card_cvc); + + cardNumberView.setFocusable(true); + cardNumberView.setFocusableInTouchMode(true); + cardExpirationView.setFocusable(true); + cardExpirationView.setFocusableInTouchMode(true); + cardCVCView.setFocusable(true); + cardCVCView.setFocusableInTouchMode(true); + + cardNumberView.setOnTouchListener(null); + cardExpirationView.setOnTouchListener(null); + cardCVCView.setOnTouchListener(null); + } + + private void handleInitFormForEditing() { + Log.d(TAG, "handleInitFormForEditing()"); + + Button verifyAndSaveButton = (Button) findViewById(R.id.button_verify_and_save); + + handleEnableForm(); + initCardNumberHelper(); + initCardExpirationHelper(); + + verifyAndSaveButton.setText(R.string.button_verify_and_save); + verifyAndSaveButton.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + handleVerifyCardAndFinish(); + } + + }); + } + + private void handleInitFormForFixingError() { + Log.d(TAG, "handleInitFormForFixingError()"); + + EditText cardNumberView = (EditText) findViewById(R.id.card_number); + EditText cardExpirationView = (EditText) findViewById(R.id.card_expiration); + TextView cardCVCView = (TextView) findViewById(R.id.card_cvc); + Button verifyAndSaveButton = (Button) findViewById(R.id.button_verify_and_save); + + if (StringUtils.isEmpty(cardNumberView.getText().toString())) + cardNumberView.setText("**** **** **** " + cardInformation.get().getCardLastFour()); + + cardNumberView.setError(getString(R.string.error_your_card_could_not_be_verified)); + + if (StringUtils.isEmpty(cardExpirationView.getText().toString())) + cardExpirationView.setText(cardInformation.get().getCardExpiration()); + + cardCVCView.setText(""); + + handleEnableForm(); + initCardNumberHelper(); + initCardExpirationHelper(); + + verifyAndSaveButton.setText(R.string.button_verify_and_save); + verifyAndSaveButton.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + handleVerifyCardAndFinish(); + } + + }); + } + + private void handleInitFormForViewingSuccess() { + Log.d(TAG, "handleInitFormForViewingSuccess()"); + + Button verifyAndSaveButton = (Button) findViewById(R.id.button_verify_and_save); + EditText cardNumberView = (EditText) findViewById(R.id.card_number); + EditText cardExpirationView = (EditText) findViewById(R.id.card_expiration); + TextView cardCVCView = (TextView) findViewById(R.id.card_cvc); + + handleEnableForm(); + + cardNumberView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) + handleInitFormForEditing(); + + return false; + } + }); + cardExpirationView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) + handleInitFormForEditing(); + + return false; + } + }); + cardCVCView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) + handleInitFormForEditing(); + + return false; + } + }); + + if (StringUtils.isEmpty(cardNumberView.getText().toString())) + cardNumberView.setText("**** **** **** " + cardInformation.get().getCardLastFour()); + + if (StringUtils.isEmpty(cardExpirationView.getText().toString())) + cardExpirationView.setText(cardInformation.get().getCardExpiration()); + + cardCVCView.setText(""); + + verifyAndSaveButton.setText(R.string.button_save); + verifyAndSaveButton.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + handleSaveAutoRenewAndFinish(true); + } + + }); + } + + private void initCardNumberHelper() { + Log.d(TAG, "initCardNumberHelper()"); + + final EditText cardNumberView = (EditText) findViewById(R.id.card_number); + final EditText cardExpirationView = (EditText) findViewById(R.id.card_expiration); + final CheckBox autoRenewIsEnabled = (CheckBox) findViewById(R.id.checkbox_enable_auto_renew); + + cardNumberView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (cardNumberView.getText() != null && + cardNumberView.getText().toString().contains("*")) + { + cardNumberView.setText(""); + } + + if (autoRenewIsEnabled.isChecked()) + handleInitFormForEditing(); + + return false; + } + }); + + if (cardNumberTextWatcher != null) + cardNumberView.removeTextChangedListener(cardNumberTextWatcher); + + cardNumberTextWatcher = new TextWatcher() { + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + String cardNumber = s.toString().replace(" ", ""); + String formattedCardNumber = ""; + + for (int i = 0; i < cardNumber.length(); i++) { + if (i > 0 && i % 4 == 0) + formattedCardNumber += " "; + + formattedCardNumber += cardNumber.charAt(i); + } + + cardNumberView.removeTextChangedListener(this); + cardNumberView.setText(formattedCardNumber); + cardNumberView.setSelection(formattedCardNumber.length()); + cardNumberView.addTextChangedListener(this); + + if (!cardNumber.contains("*") && cardNumber.length() == 16) + cardExpirationView.requestFocus(); + } + }; + + cardNumberView.addTextChangedListener(cardNumberTextWatcher); + } + + private void initCardExpirationHelper() { + Log.d(TAG, "initCardExpirationHelper()"); + + final EditText cardExpirationView = (EditText) findViewById(R.id.card_expiration); + final EditText cardCvcView = (EditText) findViewById(R.id.card_cvc); + final CheckBox autoRenewIsEnabled = (CheckBox) findViewById(R.id.checkbox_enable_auto_renew); + + cardExpirationView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (autoRenewIsEnabled.isChecked()) + handleInitFormForEditing(); + + return false; + } + }); + + if (cardExpirationTextWatcher != null) + cardExpirationView.removeTextChangedListener(cardExpirationTextWatcher); + + cardExpirationTextWatcher = new TextWatcher() { + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + String formattedCardExpiration = s.toString(); + + if (lastCardExpirationLength <= formattedCardExpiration.length() && + formattedCardExpiration.length() == 2) + { + formattedCardExpiration = formattedCardExpiration + "/"; + } + + lastCardExpirationLength = formattedCardExpiration.length(); + + cardExpirationView.removeTextChangedListener(this); + cardExpirationView.setText(formattedCardExpiration); + cardExpirationView.setSelection(formattedCardExpiration.length()); + cardExpirationView.addTextChangedListener(this); + + if (formattedCardExpiration.length() == 5) + cardCvcView.requestFocus(); + } + }; + + cardExpirationView.addTextChangedListener(cardExpirationTextWatcher); + } + + private void handleRefreshForm(boolean isCallback) { + Log.d(TAG, "handleRefreshForm() is callback >> " + isCallback); + + CheckBox autoRenewIsEnabled = (CheckBox) findViewById(R.id.checkbox_enable_auto_renew); + + if (!isCallback) + autoRenewIsEnabled.setChecked(flockAccount.get().getAutoRenewEnabled()); + + autoRenewIsEnabled.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + handleRefreshForm(true); + } + }); + + if (!autoRenewIsEnabled.isChecked()) + handleInitFormAsAutoRenewDisabled(); + else { + if (!flockAccount.get().getLastStripeChargeFailed() && cardInformation.isPresent()) + handleInitFormForViewingSuccess(); + else if (flockAccount.get().getLastStripeChargeFailed() && cardInformation.isPresent()) + handleInitFormForFixingError(); + else + handleInitFormForEditing(); + } + } + + private void handleSaveAutoRenewAndFinish(final Boolean autoRenewIsEnabled) { + if (asyncTask != null) + return; + + asyncTask = new AsyncTask() { + boolean autoRenewChanged = false; + + @Override + protected void onPreExecute() { + Log.d(TAG, "handleSaveAutoRenewAndFinish()"); + setProgressBarIndeterminateVisibility(true); + setProgressBarVisibility(true); + } + + @Override + protected Bundle doInBackground(Void... params) { + Bundle result = new Bundle(); + RegistrationApi registrationApi = new RegistrationApi(getBaseContext()); + + if (flockAccount.get().getAutoRenewEnabled() == autoRenewIsEnabled) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + return result; + } + + try { + + registrationApi.setAccountAutoRenew(davAccount, autoRenewIsEnabled); + flockAccount = Optional.of((FlockAccount) registrationApi.getAccount(davAccount)); + autoRenewChanged = true; + + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (RegistrationApiException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + asyncTask = null; + setProgressBarIndeterminateVisibility(false); + setProgressBarVisibility(false); + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + if (autoRenewChanged) + Toast.makeText(getBaseContext(), + R.string.autorenew_saved, + Toast.LENGTH_LONG).show(); + + finish(); + } + else + ErrorToaster.handleDisplayToastBundledError(getBaseContext(), result); + } + }.execute(); + } + + private void handleVerifyCardAndFinish() { + if (asyncTask != null) + return; + + asyncTask = new AsyncTask() { + + @Override + protected void onPreExecute() { + Log.d(TAG, "handleVerifyCardAndFinish()"); + setProgressBarIndeterminateVisibility(true); + setProgressBarVisibility(true); + } + + private String handleGetStripeCardTokenId(String cardNumber, + String cardExpiration, + String cardCVC) + throws StripeException + { + String[] expiration = cardExpiration.split("/"); + Integer expirationMonth = Integer.valueOf(expiration[0]); + Integer expirationYear; + + if (expiration[1].length() == 4) + expirationYear = Integer.valueOf(expiration[1]); + else + expirationYear = Integer.valueOf(expiration[1]) + 2000; + + java.util.Map cardParams = new HashMap(); + java.util.Map tokenParams = new HashMap(); + + cardParams.put("number", cardNumber.replace(" ", "")); + cardParams.put("exp_month", expirationMonth); + cardParams.put("exp_year", expirationYear); + cardParams.put("cvc", cardCVC); + + tokenParams.put("card", cardParams); + + return Token.create(tokenParams, OwsRegistration.STRIPE_PUBLIC_KEY).getId(); + } + + private void handlePutStripeTokenToServer(String stripeTokenId) + throws IOException, RegistrationApiException, CardException + { + RegistrationApi registrationApi = new RegistrationApi(getBaseContext()); + registrationApi.updateAccountStripeCard(davAccount, stripeTokenId); + } + + @Override + protected Bundle doInBackground(Void... params) { + Bundle result = new Bundle(); + String cardNumber = ((TextView)findViewById(R.id.card_number)).getText().toString(); + String cardExpiration = ((TextView)findViewById(R.id.card_expiration)).getText().toString(); + String cardCVC = ((TextView)findViewById(R.id.card_cvc)).getText().toString(); + + if (StringUtils.isEmpty(cardNumber)) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_CARD_NUMBER_INVALID); + return result; + } + + if (StringUtils.isEmpty(cardExpiration) || cardExpiration.split("/").length != 2) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_CARD_EXPIRATION_INVALID); + return result; + } + + if (StringUtils.isEmpty(cardCVC) || cardCVC.length() < 1) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_CARD_CVC_INVALID); + return result; + } + + try { + + String stripeTokenId = handleGetStripeCardTokenId(cardNumber, cardExpiration, cardCVC); + handlePutStripeTokenToServer(stripeTokenId); + + if (!flockAccount.get().getAutoRenewEnabled()) + new RegistrationApi(getBaseContext()).setAccountAutoRenew(davAccount, true); + + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (CardException e) { + ErrorToaster.handleBundleError(e, result); + } catch (StripeException e) { + ErrorToaster.handleBundleError(e, result); + } catch (RegistrationApiException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + asyncTask = null; + setProgressBarIndeterminateVisibility(false); + setProgressBarVisibility(false); + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + Toast.makeText(getBaseContext(), R.string.card_verified_and_saved, Toast.LENGTH_LONG).show(); + finish(); + } + + else { + handleInitFormForEditing(); + ErrorToaster.handleDisplayToastBundledError(getBaseContext(), result); + } + } + }.execute(); + } + + private void handleGetAccountAndCardAsync() { + if (flockAccount.isPresent() && cardInformation.isPresent()) { + handleRefreshForm(false); + return; + } + + asyncTask = new AsyncTask() { + + @Override + protected void onPreExecute() { + Log.d(TAG, "handleGetAccountAndCardAsync()"); + setProgressBarIndeterminateVisibility(true); + setProgressBarVisibility(true); + } + + @Override + protected Bundle doInBackground(String... params) { + Bundle result = new Bundle(); + RegistrationApi registrationApi = new RegistrationApi(getBaseContext()); + + try { + + if (!flockAccount.isPresent()) { + + AugmentedFlockAccount augmentedAccount = registrationApi.getAccount(davAccount); + flockAccount = Optional.of((FlockAccount) augmentedAccount); + } + + if (!cardInformation.isPresent()) + cardInformation = registrationApi.getCard(davAccount); + + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (RegistrationApiException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + asyncTask = null; + setProgressBarIndeterminateVisibility(false); + setProgressBarVisibility(false); + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) + handleRefreshForm(false); + else + ErrorToaster.handleDisplayToastBundledError(getBaseContext(), result); + } + }.execute(); + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/ErrorToaster.java b/flock/src/main/java/org/anhonesteffort/flock/ErrorToaster.java new file mode 100644 index 0000000..b08b547 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/ErrorToaster.java @@ -0,0 +1,374 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.content.Context; +import android.content.OperationApplicationException; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; +import android.widget.Toast; + +import com.stripe.exception.APIConnectionException; +import com.stripe.exception.CardException; +import com.stripe.exception.StripeException; +import org.anhonesteffort.flock.crypto.InvalidMacException; +import org.anhonesteffort.flock.registration.AuthorizationException; +import org.anhonesteffort.flock.registration.RegistrationApiClientException; +import org.anhonesteffort.flock.registration.RegistrationApiException; +import org.anhonesteffort.flock.sync.AbstractDavSyncAdapter; +import org.anhonesteffort.flock.sync.OwsWebDav; +import org.anhonesteffort.flock.webdav.InvalidComponentException; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; + +import java.io.IOException; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.security.GeneralSecurityException; +import javax.net.ssl.SSLException; + +/** + * Programmer: rhodey + */ +public class ErrorToaster { + + private static final String TAG = "org.anhonesteffort.flock.ErrorToaster"; + + protected static final String KEY_STATUS_CODE = "ErrorToaster.KEY_STATUS_CODE"; + + protected static final int CODE_SUCCESS = 0; + + protected static final int CODE_UNAUTHORIZED = 1; + protected static final int CODE_REGISTRATION_API_SERVER_ERROR = 2; + protected static final int CODE_REGISTRATION_API_CLIENT_ERROR = 3; + + protected static final int CODE_DAV_SERVER_ERROR = 4; + protected static final int CODE_DAV_CLIENT_ERROR = 5; + + protected static final int CODE_CONNECTION_ERROR = 6; + protected static final int CODE_CERTIFICATE_ERROR = 7; + protected static final int CODE_UNKNOWN_IO_ERROR = 8; + + protected static final int CODE_INVALID_CIPHER_PASSPHRASE = 9; + protected static final int CODE_INVALID_MAC_ERROR = 10; + protected static final int CODE_CRYPTO_ERROR = 11; + + protected static final int CODE_EMPTY_DAV_URL = 12; + protected static final int CODE_EMPTY_ACCOUNT_ID = 13; + protected static final int CODE_ILLEGAL_ACCOUNT_ID = 14; + protected static final int CODE_ACCOUNT_ID_TAKEN = 15; + protected static final int CODE_SHORT_PASSWORD = 16; + protected static final int CODE_PASSWORDS_DO_NOT_MATCH = 17; + + protected static final int CODE_CARD_NUMBER_INVALID = 18; + protected static final int CODE_CARD_EXPIRATION_INVALID = 19; + protected static final int CODE_CARD_CVC_INVALID = 20; + protected static final int CODE_STRIPE_REJECTED_CARD = 21; + protected static final int CODE_STRIPE_CONNECTION_ERROR = 22; + protected static final int CODE_STRIPE_API_ERROR = 23; + + protected static final int CODE_SUBSCRIPTION_EXPIRED = 24; + + protected static final int CODE_ACCOUNT_MANAGER_ERROR = 25; + + protected static void handleBundleError(Exception e, Bundle bundle) { + Log.e(TAG, "handleBundleError() - ", e); + + if (e instanceof AuthorizationException) + bundle.putInt(KEY_STATUS_CODE, CODE_UNAUTHORIZED); + else if (e instanceof RegistrationApiClientException) + bundle.putInt(KEY_STATUS_CODE, CODE_REGISTRATION_API_CLIENT_ERROR); + else if (e instanceof RegistrationApiException) + bundle.putInt(KEY_STATUS_CODE, CODE_REGISTRATION_API_SERVER_ERROR); + + else if (e instanceof DavException) { + DavException ex = (DavException) e; + + if (ex.getErrorCode() == DavServletResponse.SC_UNAUTHORIZED) + bundle.putInt(KEY_STATUS_CODE, CODE_UNAUTHORIZED); + else if (ex.getErrorCode() == OwsWebDav.STATUS_PAYMENT_REQUIRED) + bundle.putInt(KEY_STATUS_CODE, CODE_SUBSCRIPTION_EXPIRED); + else + bundle.putInt(KEY_STATUS_CODE, CODE_DAV_SERVER_ERROR); + } + + else if (e instanceof PropertyParseException) { + PropertyParseException ex = (PropertyParseException) e; + bundle.putInt(KEY_STATUS_CODE, CODE_DAV_SERVER_ERROR); + } + + else if (e instanceof InvalidComponentException) { + InvalidComponentException ex = (InvalidComponentException) e; + if (ex.isServersFault()) + bundle.putInt(KEY_STATUS_CODE, CODE_DAV_SERVER_ERROR); + else + bundle.putInt(KEY_STATUS_CODE, CODE_DAV_CLIENT_ERROR); + } + + else if (e instanceof RemoteException || e instanceof OperationApplicationException) + bundle.putInt(KEY_STATUS_CODE, CODE_DAV_CLIENT_ERROR); + + else if (e instanceof SSLException) + bundle.putInt(KEY_STATUS_CODE, CODE_CERTIFICATE_ERROR); + + else if (e instanceof IOException) { + IOException ex = (IOException) e; + if (ex instanceof SocketException || + ex instanceof UnknownHostException || + ex instanceof SocketTimeoutException) + bundle.putInt(KEY_STATUS_CODE, CODE_CONNECTION_ERROR); + else + bundle.putInt(KEY_STATUS_CODE, CODE_UNKNOWN_IO_ERROR); + } + + else if (e instanceof InvalidMacException) + bundle.putInt(KEY_STATUS_CODE, CODE_INVALID_MAC_ERROR); + else if (e instanceof GeneralSecurityException) + bundle.putInt(KEY_STATUS_CODE, CODE_CRYPTO_ERROR); + + + else if (e instanceof CardException) { + final String CODE_INVALID_CARD_NUMBER = "incorrect_number"; + final String CODE_INVALID_EXPIRATION_MONTH = "invalid_expiry_month"; + final String CODE_INVALID_EXPIRATION_YEAR = "invalid_expiry_year"; + final String CODE_INVALID_CVC = "invalid_cvc"; + + if (((CardException) e).getCode().equals(CODE_INVALID_CARD_NUMBER)) + bundle.putInt(KEY_STATUS_CODE, CODE_CARD_NUMBER_INVALID); + else if (((CardException) e).getCode().equals(CODE_INVALID_EXPIRATION_MONTH)) + bundle.putInt(KEY_STATUS_CODE, CODE_CARD_EXPIRATION_INVALID); + else if (((CardException) e).getCode().equals(CODE_INVALID_EXPIRATION_YEAR)) + bundle.putInt(KEY_STATUS_CODE, CODE_CARD_EXPIRATION_INVALID); + else if (((CardException) e).getCode().equals(CODE_INVALID_CVC)) + bundle.putInt(KEY_STATUS_CODE, CODE_CARD_CVC_INVALID); + else + bundle.putInt(KEY_STATUS_CODE, CODE_STRIPE_REJECTED_CARD); + } + else if (e instanceof APIConnectionException) + bundle.putInt(KEY_STATUS_CODE, CODE_STRIPE_CONNECTION_ERROR); + else if (e instanceof StripeException) + bundle.putInt(KEY_STATUS_CODE, CODE_STRIPE_API_ERROR); + + else + Log.e(TAG, "DID NOT HANDLE THIS EXCEPTION :(", e); + } + + protected static void handleDisplayToastBundledError(Context context, Bundle bundle) { + final int ERROR_CODE = bundle.getInt(KEY_STATUS_CODE, -1); + + switch (ERROR_CODE) { + + case CODE_UNAUTHORIZED: + handleShowUnauthorizedError(context); + break; + + case CODE_REGISTRATION_API_SERVER_ERROR: + handleShowRegistrationApiServerError(context); + break; + case CODE_REGISTRATION_API_CLIENT_ERROR: + handleShowRegistrationApiClientError(context); + break; + + case CODE_DAV_SERVER_ERROR: + handleShowDavServerError(context); + break; + case CODE_DAV_CLIENT_ERROR: + handleShowDavClientError(context); + break; + + case CODE_CONNECTION_ERROR: + handleShowConnectionError(context); + break; + case CODE_CERTIFICATE_ERROR: + handleShowCertificateError(context); + break; + case CODE_UNKNOWN_IO_ERROR: + handleShowUnknownIoError(context); + break; + + case CODE_INVALID_CIPHER_PASSPHRASE: + handleShowInvalidCipherPassphraseError(context); + break; + case CODE_INVALID_MAC_ERROR: + handleShowInvalidMacErrorError(context); + break; + case CODE_CRYPTO_ERROR: + handleShowCryptoError(context); + break; + + case CODE_EMPTY_DAV_URL: + handleShowDavUrlEmpty(context); + break; + case CODE_EMPTY_ACCOUNT_ID: + handleShowAccountIdEmpty(context); + break; + case CODE_ILLEGAL_ACCOUNT_ID: + handleShowIllegalAccountId(context); + break; + case CODE_ACCOUNT_ID_TAKEN: + handleShowAccountIdTaken(context); + break; + case CODE_SHORT_PASSWORD: + handleShowAccountPasswordTooShort(context); + break; + case CODE_PASSWORDS_DO_NOT_MATCH: + handleShowPasswordsDoNotMatch(context); + break; + + case CODE_CARD_NUMBER_INVALID: + handleShowCardNumberInvalid(context); + break; + case CODE_CARD_EXPIRATION_INVALID: + handleShowCardExpirationInvalid(context); + break; + case CODE_CARD_CVC_INVALID: + handleShowCardCVCInvalid(context); + break; + case CODE_STRIPE_REJECTED_CARD: + handleShowStripeRejectedCard(context); + break; + case CODE_STRIPE_CONNECTION_ERROR: + handleShowStripeConnectionError(context); + break; + case CODE_STRIPE_API_ERROR: + handleShowStripeApiError(context); + break; + + case CODE_SUBSCRIPTION_EXPIRED: + handleShowSubscriptionExpired(context); + break; + + case CODE_ACCOUNT_MANAGER_ERROR: + handleShowAccountManagerError(context); + break; + + } + } + + public static void handleShowError(Context context, Exception e) { + Bundle bundle = new Bundle(); + + handleBundleError(e, bundle); + handleDisplayToastBundledError(context, bundle); + } + + private static void handleShowError(Context context, int stringId) { + Toast.makeText(context, stringId, Toast.LENGTH_LONG).show(); + } + + private static void handleShowErrorQuick(Context context, int stringId) { + Toast.makeText(context, stringId, Toast.LENGTH_SHORT).show(); + } + + private static void handleShowUnauthorizedError(Context context) { + handleShowError(context, R.string.error_login_unauthorized); + DavAccountHelper.invalidateAccountPassword(context); + } + + private static void handleShowRegistrationApiServerError(Context context) { + handleShowError(context, R.string.error_registration_api_server_error); + } + private static void handleShowRegistrationApiClientError(Context context) { + handleShowError(context, R.string.error_registration_api_client_error); + } + + private static void handleShowDavServerError(Context context) { + if (DavAccountHelper.isUsingOurServers(context)) + handleShowError(context, R.string.error_our_dav_server_error); + else + handleShowError(context, R.string.error_their_dav_server_error); + } + private static void handleShowDavClientError(Context context) { + handleShowError(context, R.string.error_dav_client_error); + } + + private static void handleShowConnectionError(Context context) { + handleShowError(context, R.string.error_connection_error); + } + private static void handleShowCertificateError(Context context) { + if (DavAccountHelper.isUsingOurServers(context)) + handleShowError(context, R.string.error_our_certificate_validation); + else + handleShowError(context, R.string.error_their_certificate_validation); + } + private static void handleShowUnknownIoError(Context context) { + handleShowError(context, R.string.error_unknown_io_error); + } + + private static void handleShowInvalidCipherPassphraseError(Context context) { + handleShowErrorQuick(context, R.string.error_invalid_encryption_password); + } + private static void handleShowInvalidMacErrorError(Context context) { + handleShowError(context, R.string.error_invalid_mac_error); + } + private static void handleShowCryptoError(Context context) { + handleShowError(context, R.string.error_unknown_crypto_error); + } + + private static void handleShowDavUrlEmpty(Context context) { + handleShowErrorQuick(context, R.string.error_url_cannot_be_empty); + } + private static void handleShowAccountIdEmpty(Context context) { + handleShowErrorQuick(context, R.string.error_username_empty); + } + private static void handleShowIllegalAccountId(Context context) { + handleShowErrorQuick(context, R.string.error_username_illegal); + } + private static void handleShowAccountIdTaken(Context context) { + handleShowErrorQuick(context, R.string.error_username_already_registered); + } + private static void handleShowAccountPasswordTooShort(Context context) { + handleShowErrorQuick(context, R.string.error_password_too_short); + } + private static void handleShowPasswordsDoNotMatch(Context context) { + handleShowErrorQuick(context, R.string.error_passwords_do_not_match); + } + + private static void handleShowCardNumberInvalid(Context context) { + handleShowErrorQuick(context, R.string.error_card_number_could_not_be_verified); + } + private static void handleShowCardExpirationInvalid(Context context) { + handleShowErrorQuick(context, R.string.error_card_expiration_could_not_be_verified); + } + private static void handleShowCardCVCInvalid(Context context) { + handleShowErrorQuick(context, R.string.error_card_security_code_could_not_be_verified); + } + private static void handleShowStripeRejectedCard(Context context) { + handleShowErrorQuick(context, R.string.error_your_card_could_not_be_verified); + } + private static void handleShowStripeConnectionError(Context context) { + handleShowError(context, R.string.error_connection_error); + } + private static void handleShowStripeApiError(Context context) { + handleShowError(context, R.string.error_stripe_api_error); + } + + private static void handleShowSubscriptionExpired(Context context) { + handleShowErrorQuick(context, R.string.notification_flock_subscription_expired); + } + + private static void handleShowAccountManagerError(Context context) { + handleShowError(context, R.string.error_android_account_manager_error); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/ImportAccountService.java b/flock/src/main/java/org/anhonesteffort/flock/ImportAccountService.java new file mode 100644 index 0000000..df239c0 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/ImportAccountService.java @@ -0,0 +1,100 @@ +package org.anhonesteffort.flock; + +import android.accounts.AccountManager; +import android.app.Service; +import android.os.Bundle; +import android.util.Log; + +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.auth.AccountAuthenticator; +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.crypto.InvalidMacException; +import org.anhonesteffort.flock.crypto.KeyHelper; +import org.anhonesteffort.flock.crypto.KeyStore; +import org.anhonesteffort.flock.sync.key.KeySyncScheduler; +import org.anhonesteffort.flock.sync.key.KeySyncUtil; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.jackrabbit.webdav.DavException; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +import javax.net.ssl.SSLException; + +/** + * Created by rhodey + */ +public abstract class ImportAccountService extends Service { + + private static final String TAG = "org.anhonesteffort.flock.ImportAccountService"; + + private void handleImportOrGenerateKeyMaterial(Bundle result, + DavAccount account, + String cipherPassphrase) + { + Optional saltAndEncryptedKeyMaterial = Optional.absent(); + KeyStore.saveMasterPassphrase(getBaseContext(), cipherPassphrase); + + try { + + saltAndEncryptedKeyMaterial = KeySyncUtil.getSaltAndEncryptedKeyMaterial(getBaseContext(), account); + + } catch (PropertyParseException e) { + ErrorToaster.handleBundleError(e, result); + } catch (DavException e) { + ErrorToaster.handleBundleError(e, result); + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + + try { + + if (saltAndEncryptedKeyMaterial.isPresent()) + KeyHelper.importSaltAndEncryptedKeyMaterial(getBaseContext(), saltAndEncryptedKeyMaterial.get()); + else + KeyHelper.generateAndSaveSaltAndKeyMaterial(getBaseContext()); + + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (InvalidMacException e) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_INVALID_CIPHER_PASSPHRASE); + } catch (GeneralSecurityException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + Log.e(TAG, "handleImportOrGenerateKeyMaterial()", e); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_CRYPTO_ERROR); + } + } + + private void handleInvalidateEverything() { + DavAccountHelper.invalidateAccount(getBaseContext()); + KeyStore.invalidateKeyMaterial(getBaseContext()); + } + + protected Bundle handleImportAccount(Bundle result, + DavAccount account, + String cipherPassphrase) + { + handleInvalidateEverything(); + handleImportOrGenerateKeyMaterial(result, account, cipherPassphrase); + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + AccountManager.get(getBaseContext()).addAccountExplicitly(account.getOsAccount(), "", null); + + DavAccountHelper.setAccountUsername(getBaseContext(), account.getUserId()); + DavAccountHelper.setAccountPassword(getBaseContext(), account.getAuthToken()); + DavAccountHelper.setAccountDavHREF(getBaseContext(), account.getDavHostHREF()); + + AccountAuthenticator.setAllowAccountRemoval(getBaseContext(), false); + new KeySyncScheduler(getBaseContext()).requestSync(); + } + else + handleInvalidateEverything(); + + return result; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/ImportCalendarsActivity.java b/flock/src/main/java/org/anhonesteffort/flock/ImportCalendarsActivity.java new file mode 100644 index 0000000..ab46c6f --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/ImportCalendarsActivity.java @@ -0,0 +1,64 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentTransaction; +import android.view.MenuItem; +import android.view.Window; + +/** + * Programmer: rhodey + */ +public class ImportCalendarsActivity extends FragmentActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + requestWindowFeature(Window.FEATURE_PROGRESS); + + setContentView(R.layout.activity_with_action_button); + getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setTitle(R.string.title_import_calendars); + + FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + Fragment importCalendarsFragment = new ImportCalendarsFragment(); + + importCalendarsFragment.setHasOptionsMenu(false); + fragmentTransaction.replace(R.id.fragment_view, importCalendarsFragment); + fragmentTransaction.commit(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + break; + } + + return false; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/ImportCalendarsFragment.java b/flock/src/main/java/org/anhonesteffort/flock/ImportCalendarsFragment.java new file mode 100644 index 0000000..29889e4 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/ImportCalendarsFragment.java @@ -0,0 +1,439 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.Toast; + +import com.chiralcode.colorpicker.ColorPicker; +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.sync.calendar.LocalCalendarStore; +import org.anhonesteffort.flock.sync.calendar.LocalEventCollection; + +import java.util.LinkedList; +import java.util.List; + +/** + * Programmer: rhodey + */ +public class ImportCalendarsFragment extends AccountAndKeyRequiredFragment + implements CompoundButton.OnCheckedChangeListener +{ + private static final String TAG = "org.anhonesteffort.flock.ImportCalendarsFragment"; + + private AsyncTask asyncTask; + private ListView localCalendarListView; + private SetupActivity setupActivity; + private AlertDialog alertDialog; + + private List selectedCalendars; + private List calendarsForCopyService = new LinkedList(); + private boolean list_is_initializing = false; + private int indexOfCalendarToPrompt = -1; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + if (activity instanceof SetupActivity) + this.setupActivity = (SetupActivity) activity; + } + + @Override + public View onCreateView(LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState) + { + View fragmentView = inflater.inflate(R.layout.fragment_simple_list, container, false); + + if (!accountAndKeyAvailable()) + return fragmentView; + + initButtons(); + + return fragmentView; + } + + @Override + public void onResume() { + super.onResume(); + + if (!accountAndKeyAvailable()) + return ; + + initializeList(); + } + + @Override + public void onPause() { + super.onPause(); + + if (alertDialog != null) + alertDialog.dismiss(); + + if (asyncTask != null && !asyncTask.isCancelled()) + asyncTask.cancel(true); + } + + private void handleBackgroundImportStarted() { + Log.d(TAG, "handleBackgroundImportStarted()"); + + if (selectedCalendars.size() > 0) { + String toastMessage = getString(R.string.started_background_import_of) + " " + + selectedCalendars.size() + " " + + getString(R.string.calendars); + Toast.makeText(getActivity(), toastMessage, Toast.LENGTH_SHORT).show(); + } + + if (setupActivity != null) + setupActivity.updateFragmentUsingState(SetupActivity.STATE_SELECT_REMOTE_ADDRESSBOOK); + else + getActivity().finish(); + } + + private void handlePromptComplete(LocalEventCollection importCalendar, + String calendarName, + int calendarColor) + { + try { + + calendarsForCopyService.add( + new CalendarForCopy( + importCalendar.getAccount(), + importCalendar.getLocalId(), + calendarName, + calendarColor, + importCalendar.getComponentIds().size() + ) + ); + + indexOfCalendarToPrompt++; + handlePromptForNextCalendarNameAndColors(); + + } catch (RemoteException e) { + indexOfCalendarToPrompt = -1; + selectedCalendars.clear(); + initializeList(); + } + } + + private void handlePromptForCalendar(final LocalEventCollection importCalendar) throws RemoteException { + LayoutInflater inflater = getActivity().getLayoutInflater(); + View view = inflater.inflate(R.layout.dialog_calendar_edit, null); + final EditText displayNameEdit = (EditText ) view.findViewById(R.id.dialog_display_name); + final ColorPicker colorPicker = (ColorPicker) view.findViewById(R.id.dialog_calendar_color); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + + Optional displayName = importCalendar.getDisplayName(); + if (displayName.isPresent()) + displayNameEdit.setText(displayName.get()); + + Optional color = importCalendar.getColor(); + if (color.isPresent()) + colorPicker.setColor(color.get()); + + builder.setView(view).setTitle(R.string.title_calendar_properties); + builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + if (TextUtils.isEmpty(displayNameEdit.getText().toString())) { + Toast.makeText(getActivity(), + R.string.display_name_cannot_be_empty, + Toast.LENGTH_LONG).show(); + } + else { + handlePromptComplete(importCalendar, + displayNameEdit.getText().toString(), + colorPicker.getColor()); + } + } + + }); + + builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + indexOfCalendarToPrompt = -1; + selectedCalendars.clear(); + initializeList(); + } + + }); + + builder.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + indexOfCalendarToPrompt = -1; + selectedCalendars.clear(); + initializeList(); + } + }); + + alertDialog = builder.show(); + } + + private void handlePromptForNextCalendarNameAndColors() { + Log.d(TAG, "handlePromptForNextCalendarNameAndColors()"); + + if (indexOfCalendarToPrompt == -1) + indexOfCalendarToPrompt = 0; + + try { + + if (indexOfCalendarToPrompt < selectedCalendars.size()) + handlePromptForCalendar(selectedCalendars.get(indexOfCalendarToPrompt)); + else if (calendarsForCopyService.size() > 0) { + handleStartBackgroundCopyService(calendarsForCopyService); + handleBackgroundImportStarted(); + } + + } catch (RemoteException e) { + indexOfCalendarToPrompt = -1; + selectedCalendars.clear(); + initializeList(); + } + } + + private void initButtons() { + Button actionButton; + + if (setupActivity != null) { + actionButton = (Button) getActivity().findViewById(R.id.button_next); + actionButton.setText(R.string.skip); + } + else { + actionButton = (Button) getActivity().findViewById(R.id.button_action); + actionButton.setText(R.string.cancel); + } + + actionButton.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View view) { + if (selectedCalendars.size() > 0) + handlePromptForNextCalendarNameAndColors(); + else + handleBackgroundImportStarted(); + } + + }); + } + + private void initializeList() { + Log.d(TAG, "initializeList()"); + + if (list_is_initializing) + return; + + selectedCalendars = new LinkedList(); + list_is_initializing = true; + calendarsForCopyService = new LinkedList(); + indexOfCalendarToPrompt = -1; + + asyncTask = new RetrieveCalendarsTask().execute(); + } + + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean b) { + Button actionButton; + + if (setupActivity != null) + actionButton = (Button) getActivity().findViewById(R.id.button_next); + else + actionButton = (Button) getActivity().findViewById(R.id.button_action); + + if (selectedCalendars.size() > 0 && setupActivity != null) + actionButton.setText(R.string.next); + else if (selectedCalendars.size() > 0 && setupActivity == null) + actionButton.setText(R.string.button_import); + else if (setupActivity != null) + actionButton.setText(R.string.skip); + else + actionButton.setText(R.string.cancel); + } + + private void handleLocalCalendarsRetrieved(List localCalendars) { + Log.d(TAG, " handleLocalCalendarsRetrieved()"); + LocalEventCollection[] localCalendarArray = new LocalEventCollection[localCalendars.size()]; + for (int i = 0; i < localCalendars.size(); i++) + localCalendarArray[i] = localCalendars.get(i); + + LocalCalendarListAdapter listAdapter = + new LocalCalendarListAdapter(getActivity().getBaseContext(), localCalendarArray, selectedCalendars, this); + + localCalendarListView = (ListView)getView().findViewById(R.id.list); + localCalendarListView.setAdapter(listAdapter); + list_is_initializing = false; + } + + private void handleStartBackgroundCopyService(final List copyCalendars) { + + final Context hackContext = getActivity().getApplicationContext(); + + new AsyncTask() { + + @Override + protected Bundle doInBackground(String... params) { + Bundle result = new Bundle(); + Intent copyService = new Intent(hackContext, CalendarCopyService.class); + + for (CalendarForCopy copyCalendar : copyCalendars) { + copyService.setAction(CalendarCopyService.ACTION_QUEUE_ACCOUNT_FOR_COPY); + copyService.putExtra(CalendarCopyService.KEY_FROM_ACCOUNT, copyCalendar.fromAccount); + copyService.putExtra(CalendarCopyService.KEY_TO_ACCOUNT, account.getOsAccount()); + copyService.putExtra(CalendarCopyService.KEY_CALENDAR_ID, copyCalendar.calendarId); + copyService.putExtra(CalendarCopyService.KEY_CALENDAR_NAME, copyCalendar.calendarName); + copyService.putExtra(CalendarCopyService.KEY_CALENDAR_COLOR, copyCalendar.calendarColor); + copyService.putExtra(CalendarCopyService.KEY_EVENT_COUNT, copyCalendar.eventCount); + + hackContext.startService(copyService); + } + + copyService.setAction(CalendarCopyService.ACTION_START_COPY); + hackContext.startService(copyService); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + Log.d(TAG, "STATUS: " + result.getInt(ErrorToaster.KEY_STATUS_CODE)); + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) != ErrorToaster.CODE_SUCCESS) + ErrorToaster.handleDisplayToastBundledError(hackContext, result); + } + + }.execute(); + } + + private class RetrieveCalendarsTask extends AsyncTask { + + private List localCalendars; + + @Override + protected void onPreExecute() { + Log.d(TAG, "RetrieveCalendarsTask()"); + getActivity().setProgressBarIndeterminateVisibility(true); + getActivity().setProgressBarVisibility(true); + } + + protected List getLocalCalendars(List localAccounts) + throws RemoteException + { + List collections = new LinkedList(); + + for (Account localAccount : localAccounts) { + LocalCalendarStore localStore = new LocalCalendarStore(getActivity(), localAccount); + collections.addAll(localStore.getCollectionsIgnoreSync()); + } + + return collections; + } + + protected List getOtherAccounts() { + List accounts = new LinkedList(); + + for (Account osAccount : AccountManager.get(getActivity()).getAccounts()) { + if (!osAccount.name.equals(account.getOsAccount().name) && + !osAccount.type.equals(account.getOsAccount().type)) + { + accounts.add(osAccount); + } + } + + return accounts; + } + + @Override + protected Bundle doInBackground(Void... params) { + Bundle result = new Bundle(); + List otherAccounts = getOtherAccounts(); + + try { + + localCalendars = getLocalCalendars(otherAccounts); + + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (RemoteException e) { + ErrorToaster.handleBundleError(e, result); + } + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + Log.d(TAG, "STATUS: " + result.getInt(ErrorToaster.KEY_STATUS_CODE)); + getActivity().setProgressBarIndeterminateVisibility(false); + getActivity().setProgressBarVisibility(false); + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) + handleLocalCalendarsRetrieved(localCalendars); + else + ErrorToaster.handleDisplayToastBundledError(getActivity(), result); + } + } + + private static class CalendarForCopy { + + public Account fromAccount; + public long calendarId; + public String calendarName; + public int calendarColor; + public int eventCount; + + public CalendarForCopy(Account fromAccount, + long calendarId, + String calendarName, + int calendarColor, + int eventCount) + { + this.fromAccount = fromAccount; + this.calendarId = calendarId; + this.calendarName = calendarName; + this.calendarColor = calendarColor; + this.eventCount = eventCount; + } + + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/ImportContactsActivity.java b/flock/src/main/java/org/anhonesteffort/flock/ImportContactsActivity.java new file mode 100644 index 0000000..82cba32 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/ImportContactsActivity.java @@ -0,0 +1,64 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentTransaction; +import android.view.MenuItem; +import android.view.Window; + +/** + * Programmer: rhodey + */ +public class ImportContactsActivity extends FragmentActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + requestWindowFeature(Window.FEATURE_PROGRESS); + + setContentView(R.layout.activity_with_action_button); + getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setTitle(R.string.title_import_contacts); + + FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + Fragment importContactsFragment = new ImportContactsFragment(); + + importContactsFragment.setHasOptionsMenu(false); + fragmentTransaction.replace(R.id.fragment_view, importContactsFragment); + fragmentTransaction.commit(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + break; + } + + return false; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/ImportContactsFragment.java b/flock/src/main/java/org/anhonesteffort/flock/ImportContactsFragment.java new file mode 100644 index 0000000..275c0e6 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/ImportContactsFragment.java @@ -0,0 +1,290 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.ContentProviderClient; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.RemoteException; +import android.provider.ContactsContract; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.ListView; +import android.widget.Toast; + +import org.anhonesteffort.flock.sync.addressbook.AddressbookSyncScheduler; +import org.anhonesteffort.flock.sync.addressbook.LocalContactCollection; + +import java.util.LinkedList; +import java.util.List; + +/** + * Programmer: rhodey + */ +public class ImportContactsFragment extends AccountAndKeyRequiredFragment + implements CompoundButton.OnCheckedChangeListener +{ + private static final String TAG = "org.anhonesteffort.flock.ImportContactsFragment"; + + private AsyncTask asyncTask; + private ListView accountDetailsListView; + private SetupActivity setupActivity; + + private List selectedAccounts; + private boolean list_is_initializing = false; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + if (activity instanceof SetupActivity) + this.setupActivity = (SetupActivity) activity; + } + + @Override + public View onCreateView(LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState) + { + View fragmentView = inflater.inflate(R.layout.fragment_simple_list, container, false); + + if (!accountAndKeyAvailable()) + return fragmentView; + + initButtons(); + + return fragmentView; + } + + @Override + public void onResume() { + super.onResume(); + + if (!accountAndKeyAvailable()) + return ; + + initializeList(); + } + + @Override + public void onPause() { + super.onPause(); + + if (asyncTask != null && !asyncTask.isCancelled()) + asyncTask.cancel(true); + } + + private void initButtons() { + Button actionButton; + + if (setupActivity != null) { + actionButton = (Button) getActivity().findViewById(R.id.button_next); + actionButton.setText(R.string.skip); + } + else { + actionButton = (Button) getActivity().findViewById(R.id.button_action); + actionButton.setText(R.string.cancel); + } + + actionButton.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View view) { + Intent copyService = new Intent(getActivity(), ContactCopyService.class); + + if (selectedAccounts.size() == 0) { + handleBackgroundImportStarted(); + return; + } + + for (ImportContactsFragment.AccountContactDetails copyAccount : selectedAccounts) { + copyService.setAction(ContactCopyService.ACTION_QUEUE_ACCOUNT_FOR_COPY); + copyService.putExtra(ContactCopyService.KEY_FROM_ACCOUNT, copyAccount.account); + copyService.putExtra(ContactCopyService.KEY_TO_ACCOUNT, account.getOsAccount()); + copyService.putExtra(ContactCopyService.KEY_CONTACT_COUNT, copyAccount.contact_count); + + getActivity().startService(copyService); + } + + copyService.setAction(ContactCopyService.ACTION_START_COPY); + getActivity().startService(copyService); + handleBackgroundImportStarted(); + } + + }); + } + + private void initializeList() { + Log.d(TAG, "initializeList()"); + + if (list_is_initializing) + return; + + selectedAccounts = new LinkedList(); + list_is_initializing = true; + asyncTask = new RetrieveAccountContactDetailsTask().execute(); + } + + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean b) { + Button actionButton; + + if (setupActivity != null) + actionButton = (Button) getActivity().findViewById(R.id.button_next); + else + actionButton = (Button) getActivity().findViewById(R.id.button_action); + + if (selectedAccounts.size() > 0 && setupActivity != null) + actionButton.setText(R.string.next); + else if (selectedAccounts.size() > 0 && setupActivity == null) + actionButton.setText(R.string.button_import); + else if (setupActivity != null) + actionButton.setText(R.string.skip); + else + actionButton.setText(R.string.cancel); + } + + private void handleBackgroundImportStarted() { + Log.d(TAG, "handleBackgroundImportStarted()"); + Integer contactCount = 0; + for (ImportContactsFragment.AccountContactDetails copyAccount : selectedAccounts) + contactCount += copyAccount.contact_count; + + if (selectedAccounts.size() > 0) { + String toastMessage = getString(R.string.started_background_import_of) + " " + + contactCount + " " + getString(R.string.contacts); + Toast.makeText(getActivity(), toastMessage, Toast.LENGTH_SHORT).show(); + } + + if (setupActivity != null) + setupActivity.updateFragmentUsingState(SetupActivity.STATE_IMPORT_CALENDARS); + else + getActivity().finish(); + } + + private void handleAccountDetailsRetrieved(List accountDetails) { + Log.d(TAG, "handleAccountDetailsRetrieved()"); + AccountContactDetails[] accountDetailsArray = new AccountContactDetails[accountDetails.size()]; + for (int i = 0; i < accountDetails.size(); i++) + accountDetailsArray[i] = accountDetails.get(i); + + AccountContactDetailsListAdapter listAdapter = + new AccountContactDetailsListAdapter(getActivity().getBaseContext(), accountDetailsArray, selectedAccounts, this); + + accountDetailsListView = (ListView)getView().findViewById(R.id.list); + accountDetailsListView.setAdapter(listAdapter); + list_is_initializing = false; + } + + private class RetrieveAccountContactDetailsTask extends AsyncTask { + + List accountDetails = new LinkedList(); + + @Override + protected void onPreExecute() { + Log.d(TAG, "RetrieveAccountContactDetailsTask()"); + getActivity().setProgressBarIndeterminateVisibility(true); + getActivity().setProgressBarVisibility(true); + } + + protected void populateAccountContactCounts(List accounts) + throws RemoteException + { + ContentProviderClient client = getActivity().getContentResolver() + .acquireContentProviderClient(AddressbookSyncScheduler.CONTENT_AUTHORITY); + + for (AccountContactDetails accountDetails : accounts) { + Uri rawContactsUri = LocalContactCollection + .getSyncAdapterUri(ContactsContract.RawContacts.CONTENT_URI, accountDetails.account); + + Cursor cursor = client.query(rawContactsUri, null, null, null, null); + accountDetails.contact_count = cursor.getCount(); + + cursor.close(); + } + } + + protected List getOtherAccounts() { + List accounts = new LinkedList(); + + for (Account osAccount : AccountManager.get(getActivity()).getAccounts()) { + if (!osAccount.name.equals(account.getOsAccount().name) && + !osAccount.type.equals(account.getOsAccount().type)) + { + accounts.add(new AccountContactDetails(osAccount, 0)); + } + } + + return accounts; + } + + @Override + protected Bundle doInBackground(Void... params) { + Bundle result = new Bundle(); + + try { + + accountDetails = getOtherAccounts(); + populateAccountContactCounts(accountDetails); + + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (RemoteException e) { + ErrorToaster.handleBundleError(e, result); + } + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + Log.d(TAG, "STATUS: " + result.getInt(ErrorToaster.KEY_STATUS_CODE)); + getActivity().setProgressBarIndeterminateVisibility(false); + getActivity().setProgressBarVisibility(false); + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) + handleAccountDetailsRetrieved(accountDetails); + else + ErrorToaster.handleDisplayToastBundledError(getActivity(), result); + } + } + + protected class AccountContactDetails { + public Account account; + public int contact_count; + + public AccountContactDetails(Account account, int contact_count) { + this.account = account; + this.contact_count = contact_count; + } + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/ImportOtherAccountFragment.java b/flock/src/main/java/org/anhonesteffort/flock/ImportOtherAccountFragment.java new file mode 100644 index 0000000..d9646e3 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/ImportOtherAccountFragment.java @@ -0,0 +1,237 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.support.v4.app.Fragment; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.util.PasswordUtil; +import org.apache.commons.lang.StringUtils; + +/** + * Programmer: rhodey + */ +public class ImportOtherAccountFragment extends Fragment { + + private static final String TAG = "org.anhonesteffort.flock.ImportOtherAccountFragment"; + + private static MessageHandler messageHandler; + private SetupActivity setupActivity; + private TextWatcher passwordWatcher; + private TextWatcher passwordRepeatWatcher; + + private Optional davTestHost = Optional.absent(); + private Optional davTestUsername = Optional.absent(); + + protected void setDavTestOptions(String davTestHost, String davTestUsername) { + this.davTestHost = Optional.of(davTestHost); + this.davTestUsername = Optional.of(davTestUsername); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + if (activity instanceof SetupActivity) + this.setupActivity = (SetupActivity) activity; + else + throw new ClassCastException(activity.toString() + " not what I expected D: !"); + + if (messageHandler == null) + messageHandler = new MessageHandler(setupActivity, this); + else { + messageHandler.setupActivity = setupActivity; + messageHandler.importFragment = this; + } + } + + @Override + public View onCreateView(LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState) + { + View fragmentView = inflater.inflate(R.layout.fragment_import_other_account, container, false); + + initFormWithTestParams(fragmentView); + initButtons(); + + return fragmentView; + } + + private void initFormWithTestParams(View fragmentView) { + if (messageHandler != null && messageHandler.serviceStarted) { + getActivity().setProgressBarIndeterminateVisibility(true); + getActivity().setProgressBarVisibility(true); + setupActivity.setNavigationDisabled(true); + } + + TextView webDAVHost = (TextView)fragmentView.findViewById(R.id.href_webdav_host); + TextView webDAVUsername = (TextView)fragmentView.findViewById(R.id.account_username); + + if (davTestHost.isPresent()) + webDAVHost.setText(davTestHost.get()); + if (davTestUsername.isPresent()) + webDAVUsername.setText(davTestUsername.get()); + + final EditText passwordTextView = (EditText) fragmentView.findViewById(R.id.cipher_passphrase); + final EditText passwordRepeatTextView = (EditText) fragmentView.findViewById(R.id.cipher_passphrase_repeat); + final ProgressBar passwordProgressView = (ProgressBar) fragmentView.findViewById(R.id.progress_password_strength); + final ProgressBar passwordRepeatProgressView = (ProgressBar) fragmentView.findViewById(R.id.progress_password_strength_repeat); + + if (passwordWatcher != null) + passwordTextView.removeTextChangedListener(passwordWatcher); + if (passwordRepeatWatcher != null) + passwordRepeatTextView.removeTextChangedListener(passwordRepeatWatcher); + + passwordWatcher = PasswordUtil.getPasswordStrengthTextWatcher(getActivity(), passwordProgressView); + passwordRepeatWatcher = PasswordUtil.getPasswordStrengthTextWatcher(getActivity(), passwordRepeatProgressView); + + passwordTextView.addTextChangedListener(passwordWatcher); + passwordRepeatTextView.addTextChangedListener(passwordRepeatWatcher); + } + + private void handleImportComplete() { + Log.d(TAG, "handleImportComplete()"); + setupActivity.setIsNewAccount(true); + setupActivity.updateFragmentUsingState(SetupActivity.STATE_IMPORT_CONTACTS); + } + + private void initButtons() { + getActivity().findViewById(R.id.button_next).setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + ImportAccountAsync(); + } + + }); + } + + private void ImportAccountAsync() { + if (messageHandler != null && messageHandler.serviceStarted) + return; + else if (messageHandler == null) + messageHandler = new MessageHandler(setupActivity, this); + + Bundle result = new Bundle(); + String hrefWebDAVHost = ((TextView)getView().findViewById(R.id.href_webdav_host)).getText().toString().trim(); + String accountUsername = ((TextView)getView().findViewById(R.id.account_username)).getText().toString().trim(); + String accountPassword = ((TextView)getView().findViewById(R.id.account_password)).getText().toString().trim(); + String cipherPassphrase = ((TextView)getView().findViewById(R.id.cipher_passphrase)).getText().toString().trim(); + String cipherPassphraseRepeat = ((TextView)getView().findViewById(R.id.cipher_passphrase_repeat)).getText().toString().trim(); + + if (StringUtils.isEmpty(hrefWebDAVHost)) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_EMPTY_DAV_URL); + ErrorToaster.handleDisplayToastBundledError(getActivity(), result); + return ; + } + + if (!accountUsername.contains("@")) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_ILLEGAL_ACCOUNT_ID); + ErrorToaster.handleDisplayToastBundledError(getActivity(), result); + return ; + } + + if (StringUtils.isEmpty(cipherPassphrase) || StringUtils.isEmpty(accountPassword)) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SHORT_PASSWORD); + ErrorToaster.handleDisplayToastBundledError(getActivity(), result); + ((TextView)getView().findViewById(R.id.cipher_passphrase)).setText(""); + ((TextView)getView().findViewById(R.id.cipher_passphrase_repeat)).setText(""); + return ; + } + + if (!cipherPassphrase.equals(cipherPassphraseRepeat)) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_PASSWORDS_DO_NOT_MATCH); + ErrorToaster.handleDisplayToastBundledError(getActivity(), result); + ((TextView)getView().findViewById(R.id.cipher_passphrase)).setText(""); + ((TextView)getView().findViewById(R.id.cipher_passphrase_repeat)).setText(""); + return; + } + + DavAccount importAccount = new DavAccount(accountUsername, accountPassword, hrefWebDAVHost); + Intent importService = new Intent(getActivity(), ImportOtherAccountService.class); + + importService.putExtra(ImportOtherAccountService.KEY_MESSENGER, new Messenger(messageHandler)); + importService.putExtra(ImportOtherAccountService.KEY_ACCOUNT, importAccount.toBundle()); + importService.putExtra(ImportOtherAccountService.KEY_MASTER_PASSPHRASE, cipherPassphrase); + + getActivity().startService(importService); + messageHandler.serviceStarted = true; + + setupActivity.setNavigationDisabled(true); + getActivity().setProgressBarIndeterminateVisibility(true); + getActivity().setProgressBarVisibility(true); + } + + public static class MessageHandler extends Handler { + + public SetupActivity setupActivity; + public ImportOtherAccountFragment importFragment; + public boolean serviceStarted = false; + + public MessageHandler(SetupActivity setupActivity, ImportOtherAccountFragment importFragment) { + this.setupActivity = setupActivity; + this.importFragment = importFragment; + } + + @Override + public void handleMessage(Message message) { + messageHandler = null; + serviceStarted = false; + + setupActivity.setProgressBarIndeterminateVisibility(false); + setupActivity.setProgressBarVisibility(false); + setupActivity.setNavigationDisabled(false); + + if (message.arg1 == ErrorToaster.CODE_SUCCESS) + importFragment.handleImportComplete(); + + else { + Bundle errorBundler = new Bundle(); + + errorBundler.putInt(ErrorToaster.KEY_STATUS_CODE, message.arg1); + ErrorToaster.handleDisplayToastBundledError(setupActivity, errorBundler); + + if (importFragment.getView().findViewById(R.id.account_password) != null) { + ((TextView)importFragment.getView().findViewById(R.id.account_password)).setText(""); + ((TextView)importFragment.getView().findViewById(R.id.cipher_passphrase)).setText(""); + ((TextView)importFragment.getView().findViewById(R.id.cipher_passphrase_repeat)).setText(""); + } + } + } + + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/ImportOtherAccountService.java b/flock/src/main/java/org/anhonesteffort/flock/ImportOtherAccountService.java new file mode 100644 index 0000000..eeb8705 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/ImportOtherAccountService.java @@ -0,0 +1,196 @@ +package org.anhonesteffort.flock; + +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.support.v4.app.NotificationCompat; +import android.util.Log; + +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.jackrabbit.webdav.DavException; + +import java.io.IOException; + +/** + * Created by rhodey + */ +public class ImportOtherAccountService extends ImportAccountService { + + private static final String TAG = "org.anhonesteffort.flock.ImportOtherAccountService"; + + private static final String KEY_INTENT = "ImportOtherAccountService.KEY_INTENT"; + protected static final String KEY_MESSENGER = "ImportOtherAccountService.KEY_MESSENGER"; + protected static final String KEY_ACCOUNT = "ImportOtherAccountService.KEY_ACCOUNT"; + protected static final String KEY_MASTER_PASSPHRASE = "ImportOtherAccountService.KEY_MASTER_PASSPHRASE"; + + private Looper serviceLooper; + private ServiceHandler serviceHandler; + private NotificationManager notifyManager; + private NotificationCompat.Builder notificationBuilder; + + private Messenger messenger; + private String masterPassphrase; + private DavAccount importAccount; + + private int resultCode; + private boolean remoteActivityIsAlive = true; + + private void handleInitializeNotification() { + Log.d(TAG, "handleInitializeNotification()"); + + notificationBuilder.setContentTitle(getString(R.string.title_import_account)) + .setContentText(getString(R.string.importing_encryption_secrets)) + .setProgress(0, 0, true) + .setSmallIcon(R.drawable.flock_actionbar_icon); + + startForeground(9004, notificationBuilder.build()); + } + + @Override + public void onDestroy() { + Log.d(TAG, "onDestroy()"); + + if (remoteActivityIsAlive || resultCode == ErrorToaster.CODE_SUCCESS) + return; + + Bundle errorBundler = new Bundle(); + errorBundler.putInt(ErrorToaster.KEY_STATUS_CODE, resultCode); + ErrorToaster.handleDisplayToastBundledError(getBaseContext(), errorBundler); + + notificationBuilder + .setProgress(0, 0, false) + .setContentText(getString(R.string.account_import_failed)); + + notifyManager.notify(9004, notificationBuilder.build()); + } + + + private void handleImportComplete() { + Log.d(TAG, "handleImportComplete()"); + + if (remoteActivityIsAlive || resultCode == ErrorToaster.CODE_SUCCESS) + stopForeground(true); + else + stopForeground(false); + + stopSelf(); + } + + private void handleDavLogin(Bundle result) { + Log.d(TAG, "handleDavLogin()"); + + try { + + if (DavAccountHelper.isAuthenticated(getBaseContext(), importAccount)) + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + else + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_UNAUTHORIZED); + + } catch (DavException e) { + ErrorToaster.handleBundleError(e, result); + } catch (PropertyParseException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + } + + private void handleStartImportOtherAccount() { + Log.d(TAG, "handleStartImportOtherAccount()"); + + Bundle result = new Bundle(); + handleInitializeNotification(); + + handleDavLogin(result); + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) + handleImportAccount(result, importAccount, masterPassphrase); + + Message message = Message.obtain(); + message.arg1 = result.getInt(ErrorToaster.KEY_STATUS_CODE); + resultCode = result.getInt(ErrorToaster.KEY_STATUS_CODE); + + try { + + messenger.send(message); + + } catch (RemoteException e) { + Log.e(TAG, "caught exception while sending message to activity >> ", e); + remoteActivityIsAlive = false; + } + + handleImportComplete(); + } + + @Override + public void onCreate() { + HandlerThread thread = new HandlerThread("ImportOtherAccountService", HandlerThread.NORM_PRIORITY); + thread.start(); + + serviceLooper = thread.getLooper(); + serviceHandler = new ServiceHandler(serviceLooper); + + notifyManager = (NotificationManager) getBaseContext().getSystemService(Context.NOTIFICATION_SERVICE); + notificationBuilder = new NotificationCompat.Builder(getBaseContext()); + } + + private final class ServiceHandler extends Handler { + + public ServiceHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + Log.d(TAG, "handleMessage()"); + Intent intent = msg.getData().getParcelable(KEY_INTENT); + + if (intent != null) { + if (intent.getExtras() != null && + intent.getExtras().get(KEY_MESSENGER) != null && + intent.getExtras().getBundle(KEY_ACCOUNT) != null && + intent.getExtras().getString(KEY_MASTER_PASSPHRASE) != null) + { + if (!DavAccount.build(intent.getExtras().getBundle(KEY_ACCOUNT)).isPresent()) { + Log.e(TAG, "received bad account bundle"); + return; + } + + messenger = (Messenger) intent.getExtras().get(KEY_MESSENGER); + importAccount = DavAccount.build(intent.getExtras().getBundle(KEY_ACCOUNT)).get(); + masterPassphrase = intent.getExtras().getString(KEY_MASTER_PASSPHRASE); + + handleStartImportOtherAccount(); + } + else + Log.e(TAG, "received intent without messenger, account or master passphrase"); + } + else + Log.e(TAG, "received message with null intent"); + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Message msg = serviceHandler.obtainMessage(); + msg.getData().putParcelable(KEY_INTENT, intent); + serviceHandler.sendMessage(msg); + + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + Log.d(TAG, "onBind()"); + return null; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/ImportOwsAccountFragment.java b/flock/src/main/java/org/anhonesteffort/flock/ImportOwsAccountFragment.java new file mode 100644 index 0000000..ea72f88 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/ImportOwsAccountFragment.java @@ -0,0 +1,228 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.support.v4.app.Fragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.auth.DavAccount; +import org.apache.commons.lang.StringUtils; + + +/** + * Programmer: rhodey + */ +public class ImportOwsAccountFragment extends Fragment { + + private static final String TAG = "org.anhonesteffort.flock.ImportOwsAccountFragment"; + + private static final String KEY_USERNAME = "KEY_USERNAME"; + + protected static final int CODE_ACCOUNT_IMPORTED = 9001; + + private static MessageHandler messageHandler; + private SetupActivity setupActivity; + + private Optional username = Optional.absent(); + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (savedInstanceState != null) + username = Optional.fromNullable(savedInstanceState.getString(KEY_USERNAME)); + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + super.onSaveInstanceState(savedInstanceState); + + TextView usernameView = (TextView)getView().findViewById(R.id.account_username); + if (usernameView.getText() != null) + savedInstanceState.putString(KEY_USERNAME, usernameView.getText().toString()); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + if (activity instanceof SetupActivity) + this.setupActivity = (SetupActivity) activity; + else + throw new ClassCastException(activity.toString() + " not what I expected D: !"); + + if (messageHandler == null) + messageHandler = new MessageHandler(setupActivity, this); + else { + messageHandler.setupActivity = setupActivity; + messageHandler.importFragment = this; + } + } + + @Override + public View onCreateView(LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_import_ows_account, container, false); + + initButtons(); + initForm(); + + return view; + } + + private void initButtons() { + getActivity().findViewById(R.id.button_next).setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + handleImportAccountAsync(); + } + + }); + } + + private void initForm() { + TextView usernameView = (TextView)getActivity().findViewById(R.id.account_username); + if (username.isPresent()) + usernameView.setText(username.get()); + + if (messageHandler != null && messageHandler.serviceStarted) { + getActivity().setProgressBarIndeterminateVisibility(true); + getActivity().setProgressBarVisibility(true); + setupActivity.setNavigationDisabled(true); + } + } + + private void handleImportComplete() { + Log.d(TAG, "handleImportComplete()"); + setupActivity.updateFragmentUsingState(SetupActivity.STATE_IMPORT_CONTACTS); + } + + private void handleSubscriptionExpired(DavAccount account) { + Log.d(TAG, "handleSubscriptionExpired()"); + Toast.makeText(getActivity(), + R.string.notification_flock_subscription_expired, + Toast.LENGTH_LONG).show(); + + Intent nextIntent = new Intent(getActivity(), ManageSubscriptionActivity.class); + nextIntent.putExtra(ManageSubscriptionActivity.KEY_DAV_ACCOUNT_BUNDLE, account.toBundle()); + startActivity(nextIntent); + } + + private void handleImportAccountAsync() { + if (messageHandler != null && messageHandler.serviceStarted) + return; + else if (messageHandler == null) + messageHandler = new MessageHandler(setupActivity, this); + + Bundle result = new Bundle(); + String accountId = ((TextView)getView().findViewById(R.id.account_username)).getText().toString().trim(); + String cipherPassphrase = ((TextView)getView().findViewById(R.id.cipher_passphrase)).getText().toString().trim(); + + if (StringUtils.isEmpty(accountId)) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_EMPTY_ACCOUNT_ID); + ErrorToaster.handleDisplayToastBundledError(getActivity(), result); + ((TextView)getView().findViewById(R.id.account_username)).setText(""); + ((TextView)getView().findViewById(R.id.cipher_passphrase)).setText(""); + return; + } + + if (cipherPassphrase.length() == 0) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SHORT_PASSWORD); + ErrorToaster.handleDisplayToastBundledError(getActivity(), result); + ((TextView)getView().findViewById(R.id.account_username)).setText(""); + ((TextView)getView().findViewById(R.id.cipher_passphrase)).setText(""); + return; + } + + Intent importService = new Intent(getActivity(), ImportOwsAccountService.class); + accountId = DavAccountHelper.correctUsername(getActivity(), accountId); + + importService.putExtra(ImportOwsAccountService.KEY_MESSENGER, new Messenger(messageHandler)); + importService.putExtra(ImportOwsAccountService.KEY_ACCOUNT_ID, accountId); + importService.putExtra(ImportOwsAccountService.KEY_MASTER_PASSPHRASE, cipherPassphrase); + + getActivity().startService(importService); + messageHandler.serviceStarted = true; + setupActivity.setNavigationDisabled(true); + + getActivity().setProgressBarIndeterminateVisibility(true); + getActivity().setProgressBarVisibility(true); + } + + public static class MessageHandler extends Handler { + + public SetupActivity setupActivity; + public ImportOwsAccountFragment importFragment; + public boolean serviceStarted = false; + + public MessageHandler(SetupActivity setupActivity, ImportOwsAccountFragment importFragment) { + this.setupActivity = setupActivity; + this.importFragment = importFragment; + } + + @Override + public void handleMessage(Message message) { + messageHandler = null; + serviceStarted = false; + + setupActivity.setProgressBarIndeterminateVisibility(false); + setupActivity.setProgressBarVisibility(false); + setupActivity.setNavigationDisabled(false); + + if (message.arg1 == CODE_ACCOUNT_IMPORTED) + importFragment.handleImportComplete(); + + else if (message.arg1 == ErrorToaster.CODE_SUBSCRIPTION_EXPIRED) { + Optional account = DavAccount.build(message.getData()); + if (account.isPresent()) + importFragment.handleSubscriptionExpired(account.get()); + else + Log.e(TAG, "unable to build account for subscription expired message!!! :("); + } + + else if (message.arg1 != ErrorToaster.CODE_SUCCESS) { + Bundle errorBundler = new Bundle(); + + errorBundler.putInt(ErrorToaster.KEY_STATUS_CODE, message.arg1); + ErrorToaster.handleDisplayToastBundledError(setupActivity, errorBundler); + + if (importFragment.getView().findViewById(R.id.account_username) != null) { + ((TextView)importFragment.getView().findViewById(R.id.account_username)).setText(""); + ((TextView)importFragment.getView().findViewById(R.id.cipher_passphrase)).setText(""); + } + } + } + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/ImportOwsAccountService.java b/flock/src/main/java/org/anhonesteffort/flock/ImportOwsAccountService.java new file mode 100644 index 0000000..0ac0d9e --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/ImportOwsAccountService.java @@ -0,0 +1,355 @@ +package org.anhonesteffort.flock; + +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.support.v4.app.NotificationCompat; +import android.util.Log; + +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.crypto.InvalidMacException; +import org.anhonesteffort.flock.crypto.KeyHelper; +import org.anhonesteffort.flock.crypto.KeyUtil; +import org.anhonesteffort.flock.crypto.MasterCipher; +import org.anhonesteffort.flock.sync.OwsWebDav; +import org.anhonesteffort.flock.sync.addressbook.AddressbookSyncScheduler; +import org.anhonesteffort.flock.sync.addressbook.LocalAddressbookStore; +import org.anhonesteffort.flock.sync.calendar.CalendarsSyncScheduler; +import org.anhonesteffort.flock.sync.calendar.HidingCalDavCollection; +import org.anhonesteffort.flock.sync.calendar.HidingCalDavStore; +import org.anhonesteffort.flock.sync.calendar.LocalCalendarStore; +import org.anhonesteffort.flock.sync.key.KeySyncUtil; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.jackrabbit.webdav.DavException; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.List; + +import javax.net.ssl.SSLException; + +/** + * Created by rhodey + */ +public class ImportOwsAccountService extends ImportAccountService { + + private static final String TAG = "org.anhonesteffort.flock.ImportOwsAccountService"; + + private static final String KEY_INTENT = "ImportOwsAccountService.KEY_INTENT"; + protected static final String KEY_MESSENGER = "ImportOwsAccountService.KEY_MESSENGER"; + protected static final String KEY_ACCOUNT_ID = "ImportOwsAccountService.KEY_ACCOUNT_ID"; + protected static final String KEY_MASTER_PASSPHRASE = "ImportOwsAccountService.KEY_OLD_MASTER_PASSPHRASE"; + + private Looper serviceLooper; + private ServiceHandler serviceHandler; + private NotificationManager notifyManager; + private NotificationCompat.Builder notificationBuilder; + + private Messenger messenger; + private String accountId; + private String masterPassphrase; + private DavAccount importAccount; + + private int resultCode; + private boolean remoteActivityIsAlive = true; + private boolean accountWasImported = false; + + private void handleInitializeNotification() { + Log.d(TAG, "handleInitializeNotification()"); + + notificationBuilder.setContentTitle(getString(R.string.title_import_account)) + .setContentText(getString(R.string.importing_contacts_and_calendars)) + .setProgress(0, 0, true) + .setSmallIcon(R.drawable.flock_actionbar_icon); + + startForeground(9003, notificationBuilder.build()); + } + + @Override + public void onDestroy() { + Log.d(TAG, "onDestroy()"); + + if (remoteActivityIsAlive || resultCode == ErrorToaster.CODE_SUCCESS) + return; + + Bundle errorBundler = new Bundle(); + errorBundler.putInt(ErrorToaster.KEY_STATUS_CODE, resultCode); + ErrorToaster.handleDisplayToastBundledError(getBaseContext(), errorBundler); + + if (accountWasImported) { + notificationBuilder + .setProgress(0, 0, false) + .setContentText(getString(R.string.account_import_completed_with_errors)); + } + else { + notificationBuilder + .setProgress(0, 0, false) + .setContentText(getString(R.string.account_import_failed)); + } + + notifyManager.notify(9003, notificationBuilder.build()); + } + + + private void handleImportComplete() { + Log.d(TAG, "handleImportComplete()"); + + if (accountWasImported && resultCode != ErrorToaster.CODE_SUCCESS) + stopForeground(false); + else if (remoteActivityIsAlive || resultCode == ErrorToaster.CODE_SUCCESS) + stopForeground(true); + else + stopForeground(false); + + stopSelf(); + } + + private void handleDavLogin(Bundle result) { + Log.d(TAG, "handleDavLogin()"); + + try { + + String authToken = KeyUtil.getAuthTokenForPassphrase(masterPassphrase); + importAccount = new DavAccount(accountId, authToken, OwsWebDav.HREF_WEBDAV_HOST); + + if (DavAccountHelper.isAuthenticated(getBaseContext(), importAccount)) { + if (DavAccountHelper.isExpired(getBaseContext(), importAccount)) + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUBSCRIPTION_EXPIRED); + else + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + } + else + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_UNAUTHORIZED); + + } catch (DavException e) { + ErrorToaster.handleBundleError(e, result); + } catch (PropertyParseException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } catch (GeneralSecurityException e) { + ErrorToaster.handleBundleError(e, result); + } + } + + private void handleImportAddressbook() { + Log.d(TAG, "handleImportAddressbook()"); + + LocalAddressbookStore localStore = new LocalAddressbookStore(getBaseContext(), importAccount); + String remotePath = OwsWebDav.getAddressbookPathForUsername(importAccount.getUserId()); + String displayName = getString(R.string.addressbook); + + if (localStore.getCollections().size() == 0) { + localStore.addCollection(remotePath, displayName); + new AddressbookSyncScheduler(getBaseContext()).requestSync(); + } + } + + private List handleRemoveKeyCollection(List collections) { + Optional keyCollection = Optional.absent(); + for (HidingCalDavCollection collection : collections) { + if (collection.getPath().contains(KeySyncUtil.PATH_KEY_COLLECTION)) + keyCollection = Optional.of(collection); + } + if (keyCollection.isPresent()) + collections.remove(keyCollection.get()); + + return collections; + } + + private void handleImportCalendars(Bundle result) { + Log.d(TAG, "handleImportCalendars()"); + + try { + + Optional masterCipher = KeyHelper.getMasterCipher(getBaseContext()); + if (!masterCipher.isPresent()) { + Log.e(TAG, "master cipher is missing at handleImportCalendars()"); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_CRYPTO_ERROR); + return; + } + + LocalCalendarStore localStore = new LocalCalendarStore(getBaseContext(), importAccount.getOsAccount()); + HidingCalDavStore remoteStore = DavAccountHelper.getHidingCalDavStore(getBaseContext(), importAccount, masterCipher.get()); + + for (HidingCalDavCollection collection : handleRemoveKeyCollection(remoteStore.getCollections())) { + Optional displayName = collection.getHiddenDisplayName(); + Optional color = collection.getHiddenColor(); + + if (displayName.isPresent()) { + if (color.isPresent()) + localStore.addCollection(collection.getPath(), displayName.get(), color.get()); + else + localStore.addCollection(collection.getPath(), displayName.get()); + } + else + localStore.addCollection(collection.getPath()); + } + + remoteStore.releaseConnections(); + new CalendarsSyncScheduler(getBaseContext()).requestSync(); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (PropertyParseException e) { + ErrorToaster.handleBundleError(e, result); + } catch (DavException e) { + ErrorToaster.handleBundleError(e, result); + } catch (InvalidMacException e) { + ErrorToaster.handleBundleError(e, result); + } catch (RemoteException e) { + ErrorToaster.handleBundleError(e, result); + } catch (GeneralSecurityException e) { + ErrorToaster.handleBundleError(e, result); + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + } + + private void handleUiCallbackSubscriptionExpired() { + Log.d(TAG, "handleUiCallbackSubscriptionExpired()"); + + Message message = Message.obtain(); + message.arg1 = ErrorToaster.CODE_SUBSCRIPTION_EXPIRED; + message.setData(importAccount.toBundle()); + + try { + + messenger.send(message); + + } catch (RemoteException e) { + Log.e(TAG, "caught exception while sending message to activity >> ", e); + remoteActivityIsAlive = false; + } + } + + private void handleUiCallbackAccountImported() { + Log.d(TAG, "handleUiCallbackAccountImported()"); + + Message message = Message.obtain(); + message.arg1 = ImportOwsAccountFragment.CODE_ACCOUNT_IMPORTED; + + try { + + messenger.send(message); + + } catch (RemoteException e) { + Log.e(TAG, "caught exception while sending message to activity >> ", e); + remoteActivityIsAlive = false; + } + } + + private void handleStartImportOwsAccount() { + Log.d(TAG, "handleStartImportOwsAccount()"); + + Bundle result = new Bundle(); + handleInitializeNotification(); + + handleDavLogin(result); + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + + handleImportAccount(result, importAccount, masterPassphrase); + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + + accountWasImported = true; + handleUiCallbackAccountImported(); + handleImportAddressbook(); + handleImportCalendars(result); + } + } + + else if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUBSCRIPTION_EXPIRED) { + resultCode = result.getInt(ErrorToaster.KEY_STATUS_CODE); + + handleUiCallbackSubscriptionExpired(); + handleImportComplete(); + return; + } + + Message message = Message.obtain(); + message.arg1 = result.getInt(ErrorToaster.KEY_STATUS_CODE); + resultCode = result.getInt(ErrorToaster.KEY_STATUS_CODE); + + try { + + if (!accountWasImported) + messenger.send(message); + + } catch (RemoteException e) { + Log.e(TAG, "caught exception while sending message to activity >> ", e); + remoteActivityIsAlive = false; + } + + handleImportComplete(); + } + + @Override + public void onCreate() { + HandlerThread thread = new HandlerThread("ImportOwsAccountService", HandlerThread.NORM_PRIORITY); + thread.start(); + + serviceLooper = thread.getLooper(); + serviceHandler = new ServiceHandler(serviceLooper); + + notifyManager = (NotificationManager)getBaseContext().getSystemService(Context.NOTIFICATION_SERVICE); + notificationBuilder = new NotificationCompat.Builder(getBaseContext()); + } + + private final class ServiceHandler extends Handler { + + public ServiceHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + Log.d(TAG, "handleMessage()"); + Intent intent = msg.getData().getParcelable(KEY_INTENT); + + if (intent != null) { + if (intent.getExtras() != null && + intent.getExtras().get(KEY_MESSENGER) != null && + intent.getExtras().getString(KEY_ACCOUNT_ID) != null && + intent.getExtras().getString(KEY_MASTER_PASSPHRASE) != null) + { + messenger = (Messenger) intent.getExtras().get(KEY_MESSENGER); + accountId = intent.getExtras().getString(KEY_ACCOUNT_ID); + masterPassphrase = intent.getExtras().getString(KEY_MASTER_PASSPHRASE); + + handleStartImportOwsAccount(); + } + else + Log.e(TAG, "received intent without messenger, account id or master passphrase"); + } + else + Log.e(TAG, "received message with null intent"); + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Message msg = serviceHandler.obtainMessage(); + msg.getData().putParcelable(KEY_INTENT, intent); + serviceHandler.sendMessage(msg); + + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + Log.d(TAG, "onBind()"); + return null; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/IntroductionFragment.java b/flock/src/main/java/org/anhonesteffort/flock/IntroductionFragment.java new file mode 100644 index 0000000..1308207 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/IntroductionFragment.java @@ -0,0 +1,67 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +/** + * Programmer: rhodey + */ +public class IntroductionFragment extends Fragment { + + private SetupActivity fragmentActivity; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + if (activity instanceof SetupActivity) + this.fragmentActivity = (SetupActivity) activity; + else + throw new ClassCastException(activity.toString() + " not what I expected D: !"); + } + + @Override + public View onCreateView(LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_intro, container, false); + initButtons(); + + return view; + } + + private void initButtons() { + getActivity().findViewById(R.id.button_next).setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + fragmentActivity.updateFragmentUsingState(SetupActivity.STATE_SELECT_SERVICE_PROVIDER); + } + + }); + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/LocalCalendarListAdapter.java b/flock/src/main/java/org/anhonesteffort/flock/LocalCalendarListAdapter.java new file mode 100644 index 0000000..61c176b --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/LocalCalendarListAdapter.java @@ -0,0 +1,151 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.accounts.Account; +import android.content.Context; +import android.os.RemoteException; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.TextView; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.sync.calendar.LocalEventCollection; + +import java.util.List; + +/** + * Programmer: rhodey + * Date: 3/13/14 + */ +public class LocalCalendarListAdapter extends ArrayAdapter + implements View.OnClickListener, CompoundButton.OnCheckedChangeListener +{ + private static final String TAG = "org.anhonesteffort.flock.LocalCalendarListAdapter"; + + private LocalEventCollection[] localCalendars; + private List selectedCalendars; + private CompoundButton.OnCheckedChangeListener checkListener; + + public LocalCalendarListAdapter(Context context, + LocalEventCollection[] localCalendars, + List selectedCalendars, + CompoundButton.OnCheckedChangeListener checkListener + ) + { + super(context, R.layout.fragment_simple_list, localCalendars); + + this.localCalendars = localCalendars; + this.selectedCalendars = selectedCalendars; + this.checkListener = checkListener; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View rowView = inflater.inflate(R.layout.row_local_calendar_details, parent, false); + View colorView = rowView.findViewById(R.id.calendar_color); + TextView displayNameView = (TextView) rowView.findViewById(R.id.calendar_display_name); + TextView accountNameView = (TextView) rowView.findViewById(R.id.calendar_account_name); + CheckBox importCheckbox = (CheckBox) rowView.findViewById(R.id.import_checkbox); + + importCheckbox.setTag(R.integer.tag_account_name, localCalendars[position].getAccount().name); + importCheckbox.setTag(R.integer.tag_account_type, localCalendars[position].getAccount().type); + importCheckbox.setTag(R.integer.tag_calendar_local_id, localCalendars[position].getLocalId()); + + accountNameView.setText(localCalendars[position].getAccount().name); + + try { + + Optional displayName = localCalendars[position].getDisplayName(); + Optional color = localCalendars[position].getColor(); + + if (displayName.isPresent()) + displayNameView.setText(displayName.get()); + else + displayNameView.setText(R.string.display_name_missing); + + if (color.isPresent()) + colorView.setBackgroundColor(color.get()); + else + colorView.setBackgroundColor(getContext().getResources().getColor(R.color.flocktheme_color)); + + } catch (RemoteException e) { + Log.e(TAG, "caught exception while trying to build calendar row view", e); + ErrorToaster.handleShowError(getContext(), e); + } + + Account account = new Account(localCalendars[position].getAccount().name, + localCalendars[position].getAccount().type); + for (LocalEventCollection selectedCalendar : selectedCalendars) { + if (selectedCalendar.getAccount().equals(account) && + selectedCalendar.getLocalId().equals(localCalendars[position].getLocalId())) + { + importCheckbox.setChecked(true); + break; + } + } + + rowView.setOnClickListener(this); + importCheckbox.setOnCheckedChangeListener(this); + + return rowView; + } + + @Override + public void onClick(View view) { + CheckBox importCheckbox = (CheckBox) view.findViewById(R.id.import_checkbox); + importCheckbox.setChecked(!importCheckbox.isChecked()); + } + + @Override + public void onCheckedChanged(CompoundButton importCheckbox, boolean isChecked) { + String accountName = (String) importCheckbox.getTag(R.integer.tag_account_name); + String accountType = (String) importCheckbox.getTag(R.integer.tag_account_type); + Long localId = (Long) importCheckbox.getTag(R.integer.tag_calendar_local_id); + Account tappedAccount = new Account(accountName, accountType); + + Optional selectedCalendar = Optional.absent(); + for (LocalEventCollection calendar : selectedCalendars) { + if (calendar.getAccount().equals(tappedAccount) && calendar.getLocalId().equals(localId)) { + selectedCalendar = Optional.of(calendar); + break; + } + } + + if (!isChecked && selectedCalendar.isPresent()) + selectedCalendars.remove(selectedCalendar.get()); + else if (isChecked && !selectedCalendar.isPresent()) { + for (LocalEventCollection calendar : localCalendars) { + if (calendar.getAccount().equals(tappedAccount) && calendar.getLocalId().equals(localId)) { + selectedCalendars.add(calendar); + break; + } + } + } + + checkListener.onCheckedChanged(importCheckbox, isChecked); + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/ManageSubscriptionActivity.java b/flock/src/main/java/org/anhonesteffort/flock/ManageSubscriptionActivity.java new file mode 100644 index 0000000..c46e9ff --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/ManageSubscriptionActivity.java @@ -0,0 +1,398 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.app.Activity; +import android.content.Intent; +import android.graphics.Color; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; +import android.util.Pair; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.Window; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.registration.RegistrationApi; +import org.anhonesteffort.flock.registration.RegistrationApiException; +import org.anhonesteffort.flock.registration.model.AugmentedFlockAccount; +import org.anhonesteffort.flock.registration.model.FlockAccount; +import org.anhonesteffort.flock.registration.model.FlockCardInformation; +import org.anhonesteffort.flock.registration.model.FlockSubscription; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; + +import javax.net.ssl.SSLException; + +import de.passsy.holocircularprogressbar.HoloCircularProgressBar; + +/** + * Programmer: rhodey + */ +public class ManageSubscriptionActivity extends Activity { + + private static final String TAG = "org.anhonesteffort.flock.ManageSubscriptionActivity"; + + public static final String KEY_DAV_ACCOUNT_BUNDLE = "KEY_DAV_ACCOUNT_BUNDLE"; + public static final String KEY_CARD_INFORMATION_BUNDLE = "KEY_CARD_INFORMATION_BUNDLE"; + + private Optional flockAccount = Optional.absent(); + private Optional cardInformation = Optional.absent(); + + private final Handler uiHandler = new Handler(); + private Timer intervalTimer = new Timer(); + + private DavAccount davAccount; + private AsyncTask asyncTask; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + requestWindowFeature(Window.FEATURE_PROGRESS); + + setContentView(R.layout.activity_manage_subscription); + getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setTitle(R.string.title_manage_subscription); + + if (savedInstanceState != null && !savedInstanceState.isEmpty()) { + if (!DavAccount.build(savedInstanceState.getBundle(KEY_DAV_ACCOUNT_BUNDLE)).isPresent()) { + finish(); + return; + } + + davAccount = DavAccount.build(savedInstanceState.getBundle(KEY_DAV_ACCOUNT_BUNDLE)).get(); + cardInformation = FlockCardInformation.build(savedInstanceState.getBundle(KEY_CARD_INFORMATION_BUNDLE)); + } + else if (getIntent().getExtras() != null) { + if (!DavAccount.build(getIntent().getExtras().getBundle(KEY_DAV_ACCOUNT_BUNDLE)).isPresent()) { + finish(); + return; + } + + davAccount = DavAccount.build(getIntent().getExtras().getBundle(KEY_DAV_ACCOUNT_BUNDLE)).get(); + cardInformation = FlockCardInformation.build(getIntent().getExtras().getBundle(KEY_CARD_INFORMATION_BUNDLE)); + } + + initButtons(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.manage_subscription, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case android.R.id.home: + finish(); + break; + + + case R.id.button_send_bitcoin: + Intent nextIntent = new Intent(getBaseContext(), SendBitcoinActivity.class); + nextIntent.putExtra(SendBitcoinActivity.KEY_DAV_ACCOUNT_BUNDLE, davAccount.toBundle()); + startActivity(nextIntent); + break; + + } + + return false; + } + + @Override + public void onResume() { + super.onResume(); + + Optional> cachedSubscriptionDetails = + RegistrationApi.getCachedSubscriptionDetails(getBaseContext()); + + if (cachedSubscriptionDetails.isPresent()) { + handleUpdateUi(cachedSubscriptionDetails.get().first, + cachedSubscriptionDetails.get().second[0], + cachedSubscriptionDetails.get().second[1], + Optional.>absent()); + handleStartPerpetualRefresh(); + } + else + handleInitSubscriptionDetailsCache(); + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + savedInstanceState.putBundle(KEY_DAV_ACCOUNT_BUNDLE, davAccount.toBundle()); + + if (cardInformation.isPresent()) + savedInstanceState.putBundle(KEY_CARD_INFORMATION_BUNDLE, cardInformation.get().toBundle()); + + super.onSaveInstanceState(savedInstanceState); + } + + @Override + public void onPause() { + super.onPause(); + + if (asyncTask != null && !asyncTask.isCancelled()) + asyncTask.cancel(true); + + if (intervalTimer != null) + intervalTimer.cancel(); + } + + private void initButtons() { + findViewById(R.id.button_edit_auto_renew).setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + Intent nextIntent = new Intent(getBaseContext(), EditAutoRenewActivity.class); + nextIntent.putExtra(EditAutoRenewActivity.KEY_DAV_ACCOUNT_BUNDLE, davAccount.toBundle()); + + if (flockAccount.isPresent()) { + nextIntent.putExtra(EditAutoRenewActivity.KEY_FLOCK_ACCOUNT_BUNDLE, + flockAccount.get().toBundle()); + } + + if (cardInformation.isPresent()) { + nextIntent.putExtra(EditAutoRenewActivity.KEY_CARD_INFORMATION_BUNDLE, + cardInformation.get().toBundle()); + } + + startActivity(nextIntent); + } + + }); + } + + private void handleUpdateSubscriptionHistory(List subscriptions) { + LinearLayout subscriptionHistory = (LinearLayout)findViewById(R.id.subscription_history); + + subscriptionHistory.removeAllViews(); + for (FlockSubscription subscription : subscriptions) { + TextView subscriptionDetails = new TextView(getBaseContext()); + View spacerView = new View(getBaseContext()); + String createDate = new SimpleDateFormat("dd/MM/yy").format(subscription.getCreateDate()); + + subscriptionDetails.setText(createDate + " - " + subscription.getDaysCredit() + + " " + getString(R.string.days)); + + LinearLayout.LayoutParams subscriptionParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + subscriptionParams.setMargins(0, 8, 0, 0); + subscriptionDetails.setLayoutParams(subscriptionParams); + subscriptionDetails.setTextAppearance(getBaseContext(), android.R.style.TextAppearance_Medium); + subscriptionDetails.setTextColor(Color.BLACK); + + LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, 1 + ); + subscriptionParams.setMargins(0, 0, 0, 0); + spacerView.setLayoutParams(spacerParams); + spacerView.setBackground(getResources().getDrawable(android.R.drawable.divider_horizontal_bright)); + + subscriptionHistory.addView(subscriptionDetails); + subscriptionHistory.addView(spacerView); + } + } + + private void handleUpdateUi(Long daysRemaining, + Boolean autoRenewEnabled, + Boolean lastChargeFailed, + Optional> subscriptions) + { + TextView daysRemainingView = (TextView)findViewById(R.id.days_remaining); + TextView autoRenewView = (TextView)findViewById(R.id.auto_renew_status); + Button editAutoRenewButton = (Button)findViewById(R.id.button_edit_auto_renew); + HoloCircularProgressBar progressBarView = (HoloCircularProgressBar)findViewById(R.id.days_remaining_progress); + int daysRemainingTrigger = getResources().getInteger(R.integer.auto_renew_trigger_days_remaining); + Long daysRemainingProgress = daysRemaining; + + if (daysRemaining < 0) { + daysRemaining = 0L; + daysRemainingProgress = 0L; + } + else if (daysRemaining > 365) + daysRemainingProgress = 365L; + + daysRemainingView.setText(daysRemaining.toString()); + progressBarView.setProgress(1.0F - ((float) daysRemainingProgress / 365.0F)); + editAutoRenewButton.setText(R.string.button_edit_payment_details); + + if (!autoRenewEnabled) { + autoRenewView.setTextColor(getResources().getColor(R.color.disaled_grey)); + autoRenewView.setText(R.string.auto_renew_disabled); + } + else if (!lastChargeFailed && daysRemaining < daysRemainingTrigger) { + autoRenewView.setTextColor(getResources().getColor(R.color.success_green)); + autoRenewView.setText(getString(R.string.processing_payment)); + } + else if (!lastChargeFailed) { + autoRenewView.setTextColor(getResources().getColor(R.color.success_green)); + autoRenewView.setText(getString(R.string.auto_renew_enabled)); + } + else { + autoRenewView.setTextColor(getResources().getColor(R.color.error_red)); + autoRenewView.setText(getString(R.string.auto_renew_error)); + } + + if (subscriptions.isPresent()) + handleUpdateSubscriptionHistory(subscriptions.get()); + } + + private void handleRefreshSubscriptionDetails() { + asyncTask = new AsyncTask() { + + @Override + protected void onPreExecute() { + Log.d(TAG, "handleRefreshSubscriptionDetails()"); + setProgressBarIndeterminateVisibility(true); + setProgressBarVisibility(true); + } + + @Override + protected Bundle doInBackground(String... params) { + Bundle result = new Bundle(); + + try { + + RegistrationApi registrationApi = new RegistrationApi(getBaseContext()); + flockAccount = Optional.of(registrationApi.getAccount(davAccount)); + cardInformation = registrationApi.getCard(davAccount); + + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (RegistrationApiException e) { + ErrorToaster.handleBundleError(e, result); + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + asyncTask = null; + setProgressBarIndeterminateVisibility(false); + setProgressBarVisibility(false); + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + handleUpdateUi(flockAccount.get().getDaysRemaining(), + flockAccount.get().getAutoRenewEnabled(), + flockAccount.get().getLastStripeChargeFailed(), + Optional.of(flockAccount.get().getSubscriptions())); + } + else + ErrorToaster.handleDisplayToastBundledError(getBaseContext(), result); + } + }.execute(); + } + + private void handleInitSubscriptionDetailsCache() { + asyncTask = new AsyncTask() { + + @Override + protected void onPreExecute() { + Log.d(TAG, "handleInitSubscriptionDetailsCache()"); + setProgressBarIndeterminateVisibility(true); + setProgressBarVisibility(true); + } + + @Override + protected Bundle doInBackground(String... params) { + Bundle result = new Bundle(); + + try { + + flockAccount = Optional.of(new RegistrationApi(getBaseContext()).getAccount(davAccount)); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (RegistrationApiException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + Log.d(TAG, "STATUS: " + result.getInt(ErrorToaster.KEY_STATUS_CODE)); + setProgressBarIndeterminateVisibility(false); + setProgressBarVisibility(false); + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + handleUpdateUi(flockAccount.get().getDaysRemaining(), + flockAccount.get().getAutoRenewEnabled(), + flockAccount.get().getLastStripeChargeFailed(), + Optional.of(flockAccount.get().getSubscriptions())); + handleStartPerpetualRefresh(); + } + else + ErrorToaster.handleDisplayToastBundledError(getBaseContext(), result); + } + }.execute(); + } + + private final Runnable refreshUiRunnable = new Runnable() { + + @Override + public void run() { + if (asyncTask == null || asyncTask.isCancelled()) + handleRefreshSubscriptionDetails(); + } + + }; + + private void handleStartPerpetualRefresh() { + intervalTimer = new Timer(); + TimerTask uiTask = new TimerTask() { + + @Override + public void run() { + uiHandler.post(refreshUiRunnable); + } + + }; + + intervalTimer.schedule(uiTask, 0, 15000); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/MyAddressbooksActivity.java b/flock/src/main/java/org/anhonesteffort/flock/MyAddressbooksActivity.java new file mode 100644 index 0000000..17a2fd6 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/MyAddressbooksActivity.java @@ -0,0 +1,63 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentTransaction; +import android.view.MenuItem; +import android.view.Window; + +/** + * Programmer: rhodey + */ +public class MyAddressbooksActivity extends FragmentActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + requestWindowFeature(Window.FEATURE_PROGRESS); + + setContentView(R.layout.simple_fragment_activity); + getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setTitle(R.string.title_my_addressbooks); + + FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + Fragment addressbooksFragment = new MyAddressbooksFragment(); + + addressbooksFragment.setHasOptionsMenu(false); + fragmentTransaction.replace(R.id.fragment_view, addressbooksFragment); + fragmentTransaction.commit(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + break; + } + + return false; + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/MyAddressbooksFragment.java b/flock/src/main/java/org/anhonesteffort/flock/MyAddressbooksFragment.java new file mode 100644 index 0000000..4f39c85 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/MyAddressbooksFragment.java @@ -0,0 +1,361 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.os.AsyncTask; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.ActionMode; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.Toast; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.crypto.InvalidMacException; +import org.anhonesteffort.flock.sync.addressbook.AddressbookSyncScheduler; +import org.anhonesteffort.flock.sync.addressbook.HidingCardDavCollection; +import org.anhonesteffort.flock.sync.addressbook.HidingCardDavStore; +import org.anhonesteffort.flock.sync.addressbook.LocalAddressbookStore; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.jackrabbit.webdav.DavException; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.LinkedList; +import java.util.List; +import javax.net.ssl.SSLException; + +/** + * Programmer: rhodey + * Date: 3/14/14 + */ +public class MyAddressbooksFragment extends AbstractMyCollectionsFragment { + + private static final String TAG = "org.anhonesteffort.flock.MyAddressbooksFragment"; + + protected void handleButtonNext() { + setupActivity.get().updateFragmentUsingState(SetupActivity.STATE_SELECT_REMOTE_CALENDARS); + } + + @Override + protected void handleHideOptionsMenuItems(Menu menu) { } + + @Override + protected void handleRestoreOptionsMenuItems(Menu menu) { } + + @Override + protected String getStringCollectionsSelected() { + return getString(R.string.addressbooks_selected); + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.collection_list_edit, menu); + mode.setTitle(getString(R.string.title_edit_selected_addressbooks)); + mode.setSubtitle(batchSelections.size() + " " + getString(R.string.addressbooks_selected)); + + return true; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + switch (item.getItemId()) { + + case R.id.edit_collection_button: + handleEditSelectedAddressbook(); + break; + + case R.id.delete_collection_button: + handleDeletebatchSelections(); + break; + + } + return false; + } + + private void handleRemoteAddressbooksRetrieved(List remoteAddressbooks) { + Log.d(TAG, "handleRemoteAddressbooksRetrieved()"); + + HidingCardDavCollection[] addressbookArray = new HidingCardDavCollection[remoteAddressbooks.size()]; + for (int i = 0; i < addressbookArray.length; i++) + addressbookArray[i] = remoteAddressbooks.get(i); + + LocalAddressbookStore localStore = new LocalAddressbookStore(getActivity(), account); + RemoteAddressbookListAdapter addressbookListAdapter = + new RemoteAddressbookListAdapter(getActivity(), addressbookArray, localStore, batchSelections); + + collectionsListView = (ListView) getView().findViewById(R.id.list); + collectionsListView.setAdapter(addressbookListAdapter); + collectionsListView.setOnItemClickListener(this); + + if (!setupActivity.isPresent()) + collectionsListView.setOnItemLongClickListener(this); + + list_is_initializing = false; + updateActionMode(); + } + + private void handleEditSelectedAddressbook() { + Log.d(TAG, "handleEditSelectedAddressbook()"); + + LayoutInflater inflater = getActivity().getLayoutInflater(); + View dialogView = inflater.inflate(R.layout.dialog_addressbook_edit, null); + final EditText displayNameEdit = (EditText) dialogView.findViewById(R.id.dialog_display_name); + Optional displayName = getDisplayNameForSelectedCollection(); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + + if (displayName.isPresent()) + displayNameEdit.setText(displayName.get()); + + builder.setView(dialogView).setTitle(R.string.title_addressbook_properties); + builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + if (TextUtils.isEmpty(displayNameEdit.getText())) { + Toast.makeText(getActivity(), + R.string.display_name_cannot_be_empty, + Toast.LENGTH_LONG).show(); + } + else { + editAddressbookAsync(batchSelections.get(0), + displayNameEdit.getText().toString()); + } + } + + }); + + alertDialog = builder.show(); + } + + private void handleDeletebatchSelections() { + Log.d(TAG, "handleDeleteSelectedAddressbook()"); + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + + builder.setTitle(R.string.title_delete_selected_calendars_dialog); + builder.setMessage(R.string.are_you_sure_you_want_to_delete_selected_addressbooks); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + List batchSelectionsCopy = new LinkedList(); + for (String path : batchSelections) + batchSelectionsCopy.add(path); + + deleteAddressbooksAsync(batchSelectionsCopy); + } + + }); + + alertDialog = builder.show(); + } + + private void editAddressbookAsync(final String remotePath, + final String newDisplayName) + { + asyncTask = new AsyncTask() { + + @Override + protected void onPreExecute() { + Log.d(TAG, "editAddressbookAsync()"); + + handleStartIndeterminateProgress(); + if (actionMode != null) + actionMode.finish(); + } + + @Override + protected Bundle doInBackground(Void... params) { + Bundle result = new Bundle(); + + try { + + HidingCardDavStore remoteStore = + DavAccountHelper.getHidingCardDavStore(getActivity(), account, masterCipher); + Optional remoteAddressbook = remoteStore.getCollection(remotePath); + + if (remoteAddressbook.isPresent()) { + remoteAddressbook.get().setHiddenDisplayName(newDisplayName); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + } + else { + Log.e(TAG, "remote addressbook at " + remotePath + " is missing"); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_DAV_SERVER_ERROR); + } + + remoteStore.releaseConnections(); + + } catch (DavException e) { + ErrorToaster.handleBundleError(e, result); + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } catch (InvalidMacException e) { + ErrorToaster.handleBundleError(e, result); + } catch (GeneralSecurityException e) { + ErrorToaster.handleBundleError(e, result); + } + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + Log.d(TAG, "STATUS: " + result.getInt(ErrorToaster.KEY_STATUS_CODE)); + + batchSelections.clear(); + handleStopIndeterminateProgress(); + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + initializeList(); + new AddressbookSyncScheduler(getActivity()).requestSync(); + } + else + ErrorToaster.handleDisplayToastBundledError(getActivity(), result); + } + }.execute(); + } + + private void deleteAddressbooksAsync(final List batchSelectionsCopy) { + asyncTask = new AsyncTask() { + + @Override + protected void onPreExecute() { + Log.d(TAG, "deleteAddressbooksAsync()"); + + handleStartIndeterminateProgress(); + if (actionMode != null) + actionMode.finish(); + } + + @Override + protected Bundle doInBackground(Void... params) { + Bundle result = new Bundle(); + + try { + + HidingCardDavStore remoteStore = + DavAccountHelper.getHidingCardDavStore(getActivity(), account, masterCipher); + + LocalAddressbookStore localStore = + new LocalAddressbookStore(getActivity(), account); + + for (String remotePath : batchSelectionsCopy) { + Log.w(TAG, "deleting remote addressbook at " + remotePath); + remoteStore.removeCollection(remotePath); + + Log.w(TAG, "deleting local addressbook at " + remotePath); + localStore.removeCollection(remotePath); + } + remoteStore.releaseConnections(); + + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (DavException e) { + ErrorToaster.handleBundleError(e, result); + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + Log.d(TAG, "STATUS: " + result.getInt(ErrorToaster.KEY_STATUS_CODE)); + + batchSelections.clear(); + handleStopIndeterminateProgress(); + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) + initializeList(); + else + ErrorToaster.handleDisplayToastBundledError(getActivity(), result); + } + }.execute(); + } + + @Override + protected void retrieveRemoteCollectionsAsync() { + asyncTask = new AsyncTask() { + + private List remoteAddressbooks = + new LinkedList(); + + @Override + protected void onPreExecute() { + Log.d(TAG, "RetrieveAddressbooksTask - onPreExecute()"); + handleStartIndeterminateProgress(); + } + + @Override + protected Bundle doInBackground(Void... params) { + Bundle result = new Bundle(); + + try { + + HidingCardDavStore remoteStore = + DavAccountHelper.getHidingCardDavStore(getActivity(), account, masterCipher); + remoteAddressbooks = remoteStore.getCollections(); + remoteStore.releaseConnections(); + + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (PropertyParseException e) { + ErrorToaster.handleBundleError(e, result); + } catch (DavException e) { + ErrorToaster.handleBundleError(e, result); + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + Log.d(TAG, "STATUS: " + result.getInt(ErrorToaster.KEY_STATUS_CODE)); + handleStopIndeterminateProgress(); + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) + handleRemoteAddressbooksRetrieved(remoteAddressbooks); + else + ErrorToaster.handleDisplayToastBundledError(getActivity(), result); + } + }.execute(); + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/MyCalendarsActivity.java b/flock/src/main/java/org/anhonesteffort/flock/MyCalendarsActivity.java new file mode 100644 index 0000000..e4bb584 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/MyCalendarsActivity.java @@ -0,0 +1,64 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentTransaction; +import android.view.MenuItem; +import android.view.Window; + +/** + * Programmer: rhodey + */ +public class MyCalendarsActivity extends FragmentActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + requestWindowFeature(Window.FEATURE_PROGRESS); + + setContentView(R.layout.simple_fragment_activity); + getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setTitle(R.string.title_my_calendars); + + FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + Fragment calendarsFragment = new MyCalendarsFragment(); + + calendarsFragment.setHasOptionsMenu(true); + fragmentTransaction.replace(R.id.fragment_view, calendarsFragment); + fragmentTransaction.commit(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + break; + } + + return false; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/MyCalendarsFragment.java b/flock/src/main/java/org/anhonesteffort/flock/MyCalendarsFragment.java new file mode 100644 index 0000000..3261a10 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/MyCalendarsFragment.java @@ -0,0 +1,530 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.RemoteException; +import android.preference.PreferenceManager; +import android.text.TextUtils; +import android.util.Log; +import android.view.ActionMode; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.Toast; + +import com.chiralcode.colorpicker.ColorPicker; +import com.google.common.base.Optional; +import org.anhonesteffort.flock.crypto.InvalidMacException; +import org.anhonesteffort.flock.sync.calendar.CalendarsSyncScheduler; +import org.anhonesteffort.flock.sync.calendar.HidingCalDavCollection; +import org.anhonesteffort.flock.sync.calendar.HidingCalDavStore; +import org.anhonesteffort.flock.sync.calendar.LocalCalendarStore; +import org.anhonesteffort.flock.sync.key.KeySyncUtil; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.jackrabbit.webdav.DavException; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; +import javax.net.ssl.SSLException; + +/** + * Programmer: rhodey + */ +public class MyCalendarsFragment extends AbstractMyCollectionsFragment + implements View.OnClickListener +{ + + private static final String TAG = "org.anhonesteffort.flock.MyCalendarsFragment"; + + protected String getStringCollectionsSelected() { + return getString(R.string.calendars_selected); + } + + protected void handleButtonNext() { + setupActivity.get().handleSetupComplete(); + } + + @Override + protected void handleHideOptionsMenuItems(Menu menu) { + if (menu.findItem(R.id.create_collection_button) == null) + return; + + menu.findItem(R.id.create_collection_button).setVisible(false); + } + + @Override + protected void handleRestoreOptionsMenuItems(Menu menu) { + if (menu.findItem(R.id.create_collection_button) == null) + activity.getMenuInflater().inflate(R.menu.calendar_list_browse, menu); + + menu.findItem(R.id.create_collection_button).setVisible(true); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case R.id.create_collection_button: + handleCreateNewCalendar(); + break; + + } + + return true; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.collection_list_edit, menu); + mode.setTitle(getString(R.string.title_edit_selected_calendars)); + mode.setSubtitle(batchSelections.size() + " " + getString(R.string.calendars_selected)); + + return true; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + switch (item.getItemId()) { + + case R.id.edit_collection_button: + handleEditSelectedCalendar(); + break; + + case R.id.delete_collection_button: + handleDeleteSelectedCalendars(); + break; + + } + return false; + } + + @Override + public void onClick(View calendarColorView) { + if (batchSelections.size() == 0) { + String collectionPath = (String) calendarColorView.getTag(R.integer.tag_collection_path); + + for(int i = 0; i < collectionsListView.getChildCount(); i++) { + View rowView = collectionsListView.getChildAt(i); + + if (rowView.getTag(R.integer.tag_collection_path).equals(collectionPath)) { + handleSelectRow(rowView); + handleEditSelectedCalendar(); + break; + } + } + } + } + + private void handleRemoteCalendarsRetrieved(List remoteCalendars) { + Log.d(TAG, "handleRemoteCalendarsRetrieved()"); + + HidingCalDavCollection[] remoteCalendarArray = new HidingCalDavCollection[remoteCalendars.size()]; + for (int i = 0; i < remoteCalendarArray.length; i++) + remoteCalendarArray[i] = remoteCalendars.get(i); + + LocalCalendarStore localCalendarStore = new LocalCalendarStore(activity, account.getOsAccount()); + RemoteCalendarListAdapter calendarListAdapter = + new RemoteCalendarListAdapter(activity, + remoteCalendarArray, + localCalendarStore, + batchSelections, + this); + + collectionsListView = (ListView) activity.findViewById(R.id.list); + collectionsListView.setAdapter(calendarListAdapter); + collectionsListView.setOnItemClickListener(this); + + if (!setupActivity.isPresent()) + collectionsListView.setOnItemLongClickListener(this); + + list_is_initializing = false; + updateActionMode(); + } + + private void handleCreateNewCalendar() { + LayoutInflater inflater = activity.getLayoutInflater(); + final View view = inflater.inflate(R.layout.dialog_calendar_edit, null); + final ColorPicker colorPicker = (ColorPicker) view.findViewById(R.id.dialog_calendar_color); + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(activity); + + colorPicker.setColor(settings.getInt(PreferencesActivity.KEY_PREF_DEFAULT_CALENDAR_COLOR, R.color.flocktheme_color)); + builder.setView(view).setTitle(R.string.title_calendar_properties); + builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + EditText displayNameEdit = (EditText) view.findViewById(R.id.dialog_display_name); + createCalendarAsync(displayNameEdit.getText().toString(), colorPicker.getColor()); + } + + }); + + alertDialog = builder.show(); + } + + private int getColorForSelectedCalendar() { + if (batchSelections.size() == 0) + return R.color.flocktheme_color; + + for(int i = 0; i < collectionsListView.getChildCount(); i++) { + View rowView = collectionsListView.getChildAt(i); + View colorView = rowView.findViewById(R.id.calendar_color); + Boolean selected = (Boolean) rowView.getTag(R.integer.tag_collection_selected); + + if (selected && colorView.getTag(R.integer.tag_calendar_color) != null) + return (Integer) colorView.getTag(R.integer.tag_calendar_color); + } + + return R.color.flocktheme_color; + } + + private void handleEditSelectedCalendar() { + Log.d(TAG, "handleEditSelectedCalendar()"); + + LayoutInflater inflater = activity.getLayoutInflater(); + View view = inflater.inflate(R.layout.dialog_calendar_edit, null); + final EditText displayNameEdit = (EditText ) view.findViewById(R.id.dialog_display_name); + final ColorPicker colorPicker = (ColorPicker) view.findViewById(R.id.dialog_calendar_color); + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + Optional displayName = getDisplayNameForSelectedCollection(); + + if (displayName.isPresent()) + displayNameEdit.setText(displayName.get()); + + colorPicker.setColor(getColorForSelectedCalendar()); + builder.setView(view).setTitle(R.string.title_calendar_properties); + builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + if (TextUtils.isEmpty(displayNameEdit.getText().toString())) { + Toast.makeText(activity, + R.string.display_name_cannot_be_empty, + Toast.LENGTH_LONG).show(); + } + else { + editCalendarAsync(batchSelections.get(0), + displayNameEdit.getText().toString(), + colorPicker.getColor()); + } + } + + }); + + alertDialog = builder.show(); + } + + private void handleDeleteSelectedCalendars() { + Log.d(TAG, "handleDeleteSelectedCalendars()"); + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + + builder.setTitle(R.string.title_delete_selected_calendars_dialog); + builder.setMessage(R.string.are_you_sure_you_want_to_delete_selected_calendars); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + List batchSelectionsCopy = new LinkedList(); + for (String path : batchSelections) + batchSelectionsCopy.add(path); + + deleteCalendarsAsync(batchSelectionsCopy); + } + + }); + + alertDialog = builder.show(); + } + + private void createCalendarAsync(final String displayName, final int color) { + asyncTask = new AsyncTask() { + + @Override + protected void onPreExecute() { + Log.d(TAG, "createCalendarAsync()"); + + handleStartIndeterminateProgress(); + if (actionMode != null) + actionMode.finish(); + } + + @Override + protected Bundle doInBackground(Void... params) { + Bundle result = new Bundle(); + String calendarUUID = UUID.randomUUID().toString(); + + try { + + HidingCalDavStore remoteStore = + DavAccountHelper.getHidingCalDavStore(activity, account, masterCipher); + Optional calendarHomeSet = remoteStore.getCalendarHomeSet(); + + if (!calendarHomeSet.isPresent()) { + Log.e(TAG, "remote calendar store is missing calendar home set"); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_DAV_SERVER_ERROR); + } + + String calendarRemotePath = calendarHomeSet.get().concat(calendarUUID + "/"); + remoteStore.addCollection(calendarRemotePath, displayName, color); + remoteStore.releaseConnections(); + + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (PropertyParseException e) { + ErrorToaster.handleBundleError(e, result); + } catch (DavException e) { + ErrorToaster.handleBundleError(e, result); + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } catch (GeneralSecurityException e) { + ErrorToaster.handleBundleError(e, result); + } + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + batchSelections.clear(); + handleStopIndeterminateProgress(); + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) + initializeList(); + else + ErrorToaster.handleDisplayToastBundledError(activity, result); + } + }.execute(); + } + + private void editCalendarAsync(final String remotePath, + final String newDisplayName, + final int newColor) + { + asyncTask = new AsyncTask() { + + @Override + protected void onPreExecute() { + Log.d(TAG, "editCalendarAsync()"); + + handleStartIndeterminateProgress(); + if (actionMode != null) + actionMode.finish(); + } + + @Override + protected Bundle doInBackground(Void... params) { + Bundle result = new Bundle(); + + try { + + HidingCalDavStore remoteStore = + DavAccountHelper.getHidingCalDavStore(activity, account, masterCipher); + Optional remoteCalendar = remoteStore.getCollection(remotePath); + + if (remoteCalendar.isPresent()) { + remoteCalendar.get().setHiddenDisplayName(newDisplayName); + remoteCalendar.get().setHiddenColor(newColor); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + } + else { + Log.e(TAG, "remote calendar at " + remotePath + " is missing"); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_DAV_SERVER_ERROR); + } + + remoteStore.releaseConnections(); + + } catch (DavException e) { + ErrorToaster.handleBundleError(e, result); + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } catch (InvalidMacException e) { + ErrorToaster.handleBundleError(e, result); + } catch (GeneralSecurityException e) { + ErrorToaster.handleBundleError(e, result); + } + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + Log.d(TAG, "STATUS: " + result.getInt(ErrorToaster.KEY_STATUS_CODE)); + + batchSelections.clear(); + handleStopIndeterminateProgress(); + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + initializeList(); + new CalendarsSyncScheduler(activity).requestSync(); + } + else + ErrorToaster.handleDisplayToastBundledError(activity, result); + } + }.execute(); + } + + private void deleteCalendarsAsync(final List batchSelectionsCopy) { + asyncTask = new AsyncTask() { + + @Override + protected void onPreExecute() { + Log.d(TAG, "deleteCalendarsAsync()"); + + handleStartIndeterminateProgress(); + if (actionMode != null) + actionMode.finish(); + } + + @Override + protected Bundle doInBackground(Void... params) { + Bundle result = new Bundle(); + + try { + + HidingCalDavStore remoteStore = + DavAccountHelper.getHidingCalDavStore(activity, account, masterCipher); + LocalCalendarStore localCalendarStore = + new LocalCalendarStore(activity, account.getOsAccount()); + + for (String remotePath : batchSelectionsCopy) { + Log.w(TAG, "deleting remote calendar at " + remotePath); + remoteStore.removeCollection(remotePath); + + Log.w(TAG, "deleting local calendar at " + remotePath); + localCalendarStore.removeCollection(remotePath); + } + + remoteStore.releaseConnections(); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (DavException e) { + ErrorToaster.handleBundleError(e, result); + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } catch (RemoteException e) { + ErrorToaster.handleBundleError(e, result); + } + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + Log.d(TAG, "STATUS: " + result.getInt(ErrorToaster.KEY_STATUS_CODE)); + + batchSelections.clear(); + handleStopIndeterminateProgress(); + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) + initializeList(); + else + ErrorToaster.handleDisplayToastBundledError(activity, result); + } + }.execute(); + } + + @Override + protected void retrieveRemoteCollectionsAsync() { + asyncTask = new AsyncTask() { + + private List remoteCalendars = + new LinkedList(); + + @Override + protected void onPreExecute() { + Log.d(TAG, "RetrieveCalendarsTask - onPreExecute()"); + handleStartIndeterminateProgress(); + } + + private List handleRemoveKeyCollection(List collections) { + Optional keyCollection = Optional.absent(); + for (HidingCalDavCollection collection : collections) { + if (collection.getPath().contains(KeySyncUtil.PATH_KEY_COLLECTION)) + keyCollection = Optional.of(collection); + } + if (keyCollection.isPresent()) + collections.remove(keyCollection.get()); + + return collections; + } + + @Override + protected Bundle doInBackground(Void... params) { + Bundle result = new Bundle(); + + try { + + HidingCalDavStore remoteCalDavStore = + DavAccountHelper.getHidingCalDavStore(activity, account, masterCipher); + remoteCalendars = handleRemoveKeyCollection(remoteCalDavStore.getCollections()); + remoteCalDavStore.releaseConnections(); + + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (PropertyParseException e) { + ErrorToaster.handleBundleError(e, result); + } catch (DavException e) { + ErrorToaster.handleBundleError(e, result); + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + Log.d(TAG, "STATUS: " + result.getInt(ErrorToaster.KEY_STATUS_CODE)); + + handleStopIndeterminateProgress(); + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) + handleRemoteCalendarsRetrieved(remoteCalendars); + else + ErrorToaster.handleDisplayToastBundledError(activity, result); + } + }.execute(); + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/PreferencesActivity.java b/flock/src/main/java/org/anhonesteffort/flock/PreferencesActivity.java new file mode 100644 index 0000000..4eb37ad --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/PreferencesActivity.java @@ -0,0 +1,228 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.EditTextPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceCategory; +import android.preference.PreferenceManager; +import android.widget.Toast; + +import com.chiralcode.colorpicker.ColorPickerPreference; +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.sync.addressbook.AddressbookSyncScheduler; +import org.anhonesteffort.flock.sync.calendar.CalendarsSyncScheduler; +import org.anhonesteffort.flock.sync.key.KeySyncScheduler; +import org.anhonesteffort.flock.util.ColorUtils; + +/** + * Programmer: rhodey + */ +public class PreferencesActivity extends PreferenceActivity + implements Preference.OnPreferenceChangeListener +{ + + public static final String KEY_PREF_SYNC_INTERVAL_MINUTES = "pref_sync_interval_minutes"; + public static final String KEY_PREF_SYNC_ON_CONTENT_CHANGE = "pref_sync_on_content_change"; + public static final String KEY_PREF_SYNC_NOW = "pref_sync_now"; + public static final String KEY_PREF_DEFAULT_CALENDAR_COLOR = "pref_default_calendar_color"; + + public static final String KEY_PREF_CATEGORY_CONTACTS = "pref_category_contacts"; + public static final String KEY_PREF_ADDRESSBOOKS = "pref_addressbooks"; + + public static final String KEY_PREF_CATEGORY_ACCOUNT = "pref_category_account"; + public static final String KEY_PREF_SUBSCRIPTION = "pref_subscription"; + public static final String KEY_PREF_DELETE_ACCOUNT = "pref_delete_account"; + + private StatusHeaderView statusHeader; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + statusHeader = new StatusHeaderView(getBaseContext()); + getListView().addHeaderView(statusHeader, null, false); + + addPreferencesFromResource(R.xml.preferences); + getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setTitle(R.string.app_name); + + findPreference(KEY_PREF_SYNC_INTERVAL_MINUTES).setOnPreferenceChangeListener(this); + findPreference(KEY_PREF_SYNC_ON_CONTENT_CHANGE).setOnPreferenceChangeListener(this); + findPreference(KEY_PREF_DEFAULT_CALENDAR_COLOR).setOnPreferenceChangeListener(this); + + initContentObservers(); + initSyncNowButton(); + } + + @Override + public void onResume() { + super.onResume(); + + if (!DavAccountHelper.isAccountRegistered(getBaseContext())) { + Intent nextIntent = new Intent(getBaseContext(), SetupActivity.class); + nextIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(nextIntent); + finish(); + } + + else { + initPreferencesForOwsUsers(); + initPreferencesForNonOwsUsers(); + updateSyncIntervalSummary(Optional.absent()); + updateCalendarColorSummary(Optional.absent()); + + statusHeader.handleStartPerpetualRefresh(); + } + } + + @Override + public void onPause() { + super.onPause(); + statusHeader.hackOnPause(); + } + + private void initContentObservers() { + new AddressbookSyncScheduler(getBaseContext()).registerSelfForBroadcasts(); + new CalendarsSyncScheduler(getBaseContext()).registerSelfForBroadcasts(); + } + + private void initSyncNowButton() { + Preference syncNowPreference = findPreference(KEY_PREF_SYNC_NOW); + syncNowPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + + @Override + public boolean onPreferenceClick(Preference preference) { + + new KeySyncScheduler(getBaseContext()).requestSync(); + new CalendarsSyncScheduler(getBaseContext()).requestSync(); + new AddressbookSyncScheduler(getBaseContext()).requestSync(); + + Toast.makeText(getBaseContext(), + R.string.sync_requested_will_begin_when_possible, + Toast.LENGTH_SHORT).show(); + + return false; + } + + }); + } + + private void updateSyncIntervalSummary(Optional value) { + EditTextPreference syncIntervalPreference = + (EditTextPreference) findPreference(KEY_PREF_SYNC_INTERVAL_MINUTES); + + if (value.isPresent()) + syncIntervalPreference.setSummary(value.get() + " " + getString(R.string.minutes)); + else { + String intervalMinutes = syncIntervalPreference.getText(); + syncIntervalPreference.setSummary(intervalMinutes + " " + getString(R.string.minutes)); + } + } + + private void updateCalendarColorSummary(Optional value) { + ColorUtils colorUtils = new ColorUtils(); + + ColorPickerPreference calendarColorPreference = + (ColorPickerPreference) findPreference(KEY_PREF_DEFAULT_CALENDAR_COLOR); + + if (value.isPresent()) { + String colorName = colorUtils.getColorNameFromHex(value.get()); + calendarColorPreference.setSummary(getString(R.string.new_calendars_will_be) + " '" + colorName + "'"); + } + else { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); + int colorFlockTheme = getResources().getColor(R.color.flocktheme_color); + Integer calendarColor = settings.getInt(KEY_PREF_DEFAULT_CALENDAR_COLOR, colorFlockTheme); + String colorName = colorUtils.getColorNameFromHex(calendarColor); + + if (colorName != null) + calendarColorPreference.setSummary(getString(R.string.new_calendars_will_be) + " '" + colorName + "'"); + } + } + + private void initPreferencesForOwsUsers() { + if (!DavAccountHelper.isUsingOurServers(getBaseContext())) + return; + + Preference manageSubscription = findPreference(KEY_PREF_SUBSCRIPTION); + Preference addressbooks = findPreference(KEY_PREF_ADDRESSBOOKS); + PreferenceCategory category = (PreferenceCategory) findPreference(KEY_PREF_CATEGORY_CONTACTS); + + if (manageSubscription != null) { + manageSubscription.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + + @Override + public boolean onPreferenceClick(Preference preference) { + Optional account = DavAccountHelper.getAccount(getBaseContext()); + if (account.isPresent()) { + Intent nextIntent = new Intent(getBaseContext(), ManageSubscriptionActivity.class); + nextIntent.putExtra(ManageSubscriptionActivity.KEY_DAV_ACCOUNT_BUNDLE, + account.get().toBundle()); + startActivity(nextIntent); + } + + return true; + } + + }); + } + + if (addressbooks != null) + category.removePreference(addressbooks); + } + + private void initPreferencesForNonOwsUsers() { + if (DavAccountHelper.isUsingOurServers(getBaseContext())) + return; + + PreferenceCategory accountCategory = (PreferenceCategory) findPreference(KEY_PREF_CATEGORY_ACCOUNT); + Preference manageSubscription = findPreference(KEY_PREF_SUBSCRIPTION); + Preference deleteAccount = findPreference(KEY_PREF_DELETE_ACCOUNT); + + if (manageSubscription != null) { + accountCategory.removePreference(manageSubscription); + accountCategory.removePreference(deleteAccount); + } + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (preference.getKey().equals(KEY_PREF_SYNC_INTERVAL_MINUTES)) { + new KeySyncScheduler(getBaseContext()).setSyncInterval(Integer.valueOf((String) newValue)); + new AddressbookSyncScheduler(getBaseContext()).setSyncInterval(Integer.valueOf((String) newValue)); + new CalendarsSyncScheduler(getBaseContext()).setSyncInterval(Integer.valueOf((String) newValue)); + + updateSyncIntervalSummary(Optional.of((String) newValue)); + } + + else if (preference.getKey().equals(KEY_PREF_DEFAULT_CALENDAR_COLOR)) + updateCalendarColorSummary(Optional.of((Integer) newValue)); + + return true; + } + +} \ No newline at end of file diff --git a/flock/src/main/java/org/anhonesteffort/flock/RegisterAccountFragment.java b/flock/src/main/java/org/anhonesteffort/flock/RegisterAccountFragment.java new file mode 100644 index 0000000..2529fd0 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/RegisterAccountFragment.java @@ -0,0 +1,236 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.support.v4.app.Fragment; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.util.PasswordUtil; +import org.apache.commons.lang.StringUtils; + +/** + * Programmer: rhodey + */ +public class RegisterAccountFragment extends Fragment { + + private static final String TAG = "org.anhonesteffort.flock.RegisterAccountFragment"; + + private static final String KEY_USERNAME = "KEY_USERNAME"; + + protected static final int CODE_ACCOUNT_IMPORTED = 9001; + + private static MessageHandler messageHandler; + private SetupActivity setupActivity; + private TextWatcher passwordWatcher; + private TextWatcher passwordRepeatWatcher; + private Optional username = Optional.absent(); + + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (savedInstanceState != null) + username = Optional.fromNullable(savedInstanceState.getString(KEY_USERNAME)); + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + super.onSaveInstanceState(savedInstanceState); + + EditText usernameView = (EditText) getActivity().findViewById(R.id.account_username); + if (usernameView.getText() != null) + savedInstanceState.putString(KEY_USERNAME, usernameView.getText().toString()); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + if (activity instanceof SetupActivity) + this.setupActivity = (SetupActivity) activity; + else + throw new ClassCastException(activity.toString() + " not what I expected D: !"); + + if (messageHandler == null) + messageHandler = new MessageHandler(setupActivity, this); + else { + messageHandler.setupActivity = setupActivity; + messageHandler.importFragment = this; + } + } + + @Override + public View onCreateView(LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_register_ows_account, container, false); + + initButtons(); + initForm(view); + + return view; + } + + private void initButtons() { + getActivity().findViewById(R.id.button_next).setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + registerAccountAsync(); + } + + }); + } + + private void initForm(View view) { + if (messageHandler != null && messageHandler.serviceStarted) { + getActivity().setProgressBarIndeterminateVisibility(true); + getActivity().setProgressBarVisibility(true); + setupActivity.setNavigationDisabled(true); + } + + if (username.isPresent()) + ((EditText)view.findViewById(R.id.account_username)).setText(username.get()); + + final EditText passwordTextView = (EditText) view.findViewById(R.id.cipher_passphrase); + final EditText passwordRepeatTextView = (EditText) view.findViewById(R.id.cipher_passphrase_repeat); + final ProgressBar passwordProgressView = (ProgressBar) view.findViewById(R.id.progress_password_strength); + final ProgressBar passwordRepeatProgressView = (ProgressBar) view.findViewById(R.id.progress_password_strength_repeat); + + if (passwordWatcher != null) + passwordTextView.removeTextChangedListener(passwordWatcher); + if (passwordRepeatWatcher != null) + passwordRepeatTextView.removeTextChangedListener(passwordRepeatWatcher); + + passwordWatcher = PasswordUtil.getPasswordStrengthTextWatcher(getActivity(), passwordProgressView); + passwordRepeatWatcher = PasswordUtil.getPasswordStrengthTextWatcher(getActivity(), passwordRepeatProgressView); + + passwordTextView.addTextChangedListener(passwordWatcher); + passwordRepeatTextView.addTextChangedListener(passwordRepeatWatcher); + } + + private void handleRegisterComplete() { + Log.d(TAG, "handleRegisterComplete()"); + setupActivity.updateFragmentUsingState(SetupActivity.STATE_IMPORT_CONTACTS); + } + + private void registerAccountAsync() { + if (messageHandler != null && messageHandler.serviceStarted) + return; + else if (messageHandler == null) + messageHandler = new MessageHandler(setupActivity, this); + + Bundle result = new Bundle(); + String username = ((EditText) getActivity().findViewById(R.id.account_username)).getText().toString().trim(); + String password = ((EditText) getActivity().findViewById(R.id.cipher_passphrase)).getText().toString().trim(); + String passwordRepeat = ((EditText) getActivity().findViewById(R.id.cipher_passphrase_repeat)).getText().toString().trim(); + + if (StringUtils.isEmpty(username)) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_EMPTY_ACCOUNT_ID); + ErrorToaster.handleDisplayToastBundledError(getActivity(), result); + return; + } + + if (StringUtils.isEmpty(password)) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SHORT_PASSWORD); + ErrorToaster.handleDisplayToastBundledError(getActivity(), result); + ((TextView)getActivity().findViewById(R.id.cipher_passphrase)).setText(""); + ((TextView)getActivity().findViewById(R.id.cipher_passphrase_repeat)).setText(""); + return; + } + + if (StringUtils.isEmpty(passwordRepeat) || !password.equals(passwordRepeat)) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_PASSWORDS_DO_NOT_MATCH); + ErrorToaster.handleDisplayToastBundledError(getActivity(), result); + ((TextView)getActivity().findViewById(R.id.cipher_passphrase)).setText(""); + ((TextView)getActivity().findViewById(R.id.cipher_passphrase_repeat)).setText(""); + return; + } + + username = DavAccountHelper.correctUsername(getActivity(), username); + Intent importService = new Intent(getActivity(), RegisterAccountService.class); + + importService.putExtra(RegisterAccountService.KEY_MESSENGER, new Messenger(messageHandler)); + importService.putExtra(RegisterAccountService.KEY_ACCOUNT_ID, username); + importService.putExtra(RegisterAccountService.KEY_MASTER_PASSPHRASE, password); + + getActivity().startService(importService); + messageHandler.serviceStarted = true; + + setupActivity.setNavigationDisabled(true); + getActivity().setProgressBarIndeterminateVisibility(true); + getActivity().setProgressBarVisibility(true); + } + + public static class MessageHandler extends Handler { + + public SetupActivity setupActivity; + public RegisterAccountFragment importFragment; + public boolean serviceStarted = false; + + public MessageHandler(SetupActivity setupActivity, RegisterAccountFragment importFragment) { + this.setupActivity = setupActivity; + this.importFragment = importFragment; + } + + @Override + public void handleMessage(Message message) { + messageHandler = null; + serviceStarted = false; + + setupActivity.setNavigationDisabled(false); + setupActivity.setProgressBarIndeterminateVisibility(false); + setupActivity.setProgressBarVisibility(false); + + if (message.arg1 == CODE_ACCOUNT_IMPORTED) + importFragment.handleRegisterComplete(); + + else if (message.arg1 != ErrorToaster.CODE_SUCCESS) { + Bundle errorBundler = new Bundle(); + + errorBundler.putInt(ErrorToaster.KEY_STATUS_CODE, message.arg1); + ErrorToaster.handleDisplayToastBundledError(setupActivity, errorBundler); + + if (importFragment.getView().findViewById(R.id.account_username) != null) { + ((TextView)importFragment.getView().findViewById(R.id.account_username)).setText(""); + ((TextView)importFragment.getView().findViewById(R.id.cipher_passphrase)).setText(""); + ((TextView)importFragment.getView().findViewById(R.id.cipher_passphrase_repeat)).setText(""); + } + } + } + + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/RegisterAccountService.java b/flock/src/main/java/org/anhonesteffort/flock/RegisterAccountService.java new file mode 100644 index 0000000..78be1ad --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/RegisterAccountService.java @@ -0,0 +1,303 @@ +package org.anhonesteffort.flock; + +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.support.v4.app.NotificationCompat; +import android.util.Log; + +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.crypto.KeyUtil; +import org.anhonesteffort.flock.registration.RegistrationApi; +import org.anhonesteffort.flock.registration.RegistrationApiException; +import org.anhonesteffort.flock.registration.ResourceAlreadyExistsException; +import org.anhonesteffort.flock.sync.OwsWebDav; +import org.anhonesteffort.flock.sync.addressbook.AddressbookSyncScheduler; +import org.anhonesteffort.flock.sync.addressbook.LocalAddressbookStore; +import org.anhonesteffort.flock.sync.calendar.CalendarsSyncScheduler; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.anhonesteffort.flock.webdav.caldav.CalDavCollection; +import org.anhonesteffort.flock.webdav.caldav.CalDavStore; +import org.apache.jackrabbit.webdav.DavException; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +import javax.net.ssl.SSLException; + +/** + * Created by rhodey + */ +public class RegisterAccountService extends ImportAccountService { + + private static final String TAG = "org.anhonesteffort.flock.RegisterAccountService"; + + private static final String KEY_INTENT = "RegisterAccountService.KEY_INTENT"; + protected static final String KEY_MESSENGER = "RegisterAccountService.KEY_MESSENGER"; + protected static final String KEY_ACCOUNT_ID = "RegisterAccountService.KEY_ACCOUNT_ID"; + protected static final String KEY_MASTER_PASSPHRASE = "RegisterAccountService.KEY_OLD_MASTER_PASSPHRASE"; + + private Looper serviceLooper; + private ServiceHandler serviceHandler; + private NotificationManager notifyManager; + private NotificationCompat.Builder notificationBuilder; + + private Messenger messenger; + private String accountId; + private String masterPassphrase; + private DavAccount registerAcount; + + private int resultCode; + private boolean remoteActivityIsAlive = true; + private boolean accountWasImported = false; + + private void handleInitializeNotification() { + Log.d(TAG, "handleInitializeNotification()"); + + notificationBuilder.setContentTitle(getString(R.string.title_register_account)) + .setContentText(getString(R.string.registering_account)) + .setProgress(0, 0, true) + .setSmallIcon(R.drawable.flock_actionbar_icon); + + startForeground(9005, notificationBuilder.build()); + } + + @Override + public void onDestroy() { + Log.d(TAG, "onDestroy()"); + + if (remoteActivityIsAlive || resultCode == ErrorToaster.CODE_SUCCESS) + return; + + Bundle errorBundler = new Bundle(); + errorBundler.putInt(ErrorToaster.KEY_STATUS_CODE, resultCode); + ErrorToaster.handleDisplayToastBundledError(getBaseContext(), errorBundler); + + if (accountWasImported) { + notificationBuilder + .setProgress(0, 0, false) + .setContentText(getString(R.string.account_import_completed_with_errors)); + } + else { + notificationBuilder + .setProgress(0, 0, false) + .setContentText(getString(R.string.account_register_failed)); + } + + notifyManager.notify(9005, notificationBuilder.build()); + } + + + private void handleImportComplete() { + Log.d(TAG, "handleImportComplete()"); + + if (remoteActivityIsAlive || resultCode == ErrorToaster.CODE_SUCCESS) + stopForeground(true); + else + stopForeground(false); + + stopSelf(); + } + + private void handleRegisterAccount(Bundle result) { + Log.d(TAG, "handleRegisterAccount()"); + + try { + + String authToken = KeyUtil.getAuthTokenForPassphrase(masterPassphrase); + RegistrationApi registrationApi = new RegistrationApi(getBaseContext()); + registerAcount = new DavAccount(accountId, authToken, OwsWebDav.HREF_WEBDAV_HOST); + + registrationApi.createAccount(registerAcount); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (ResourceAlreadyExistsException e) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_ACCOUNT_ID_TAKEN); + } catch (RegistrationApiException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } catch (GeneralSecurityException e) { + ErrorToaster.handleBundleError(e, result); + } + } + + private void handleImportAddressbook() { + LocalAddressbookStore localStore = new LocalAddressbookStore(getBaseContext(), registerAcount); + String remotePath = OwsWebDav.getAddressbookPathForUsername(registerAcount.getUserId()); + String displayName = getString(R.string.addressbook); + + if (localStore.getCollections().size() == 0) { + localStore.addCollection(remotePath, displayName); + new AddressbookSyncScheduler(getBaseContext()).requestSync(); + } + } + + private void handleDeleteDefaultCalendars(Bundle result) { + try { + + CalDavStore store = DavAccountHelper.getCalDavStore(getBaseContext(), registerAcount); + for (CalDavCollection collection : store.getCollections()) { + Log.d(TAG, "deleting default calendar " + collection.getPath()); + store.removeCollection(collection.getPath()); + } + + store.closeHttpConnection(); + new CalendarsSyncScheduler(getBaseContext()).requestSync(); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (PropertyParseException e) { + ErrorToaster.handleBundleError(e, result); + } catch (DavException e) { + ErrorToaster.handleBundleError(e, result); + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + } + + private void handleRevertRegisterAccount(Bundle result) { + Log.w(TAG, "handleRevertRegisterAccount()"); + + int statusSave = result.getInt(ErrorToaster.KEY_STATUS_CODE); + + try { + + new RegistrationApi(getBaseContext()).deleteAccount(registerAcount); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (ResourceAlreadyExistsException e) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_ACCOUNT_ID_TAKEN); + } catch (RegistrationApiException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) != ErrorToaster.CODE_SUCCESS) + Log.e(TAG, "unable to revert registed account!!! XXX :("); + + result.putInt(ErrorToaster.KEY_STATUS_CODE, statusSave); + } + + private void handleUiCallbackAccountImported() { + Log.d(TAG, "handleUiCallbackAccountImported()"); + + Message message = Message.obtain(); + message.arg1 = RegisterAccountFragment.CODE_ACCOUNT_IMPORTED; + + try { + + messenger.send(message); + + } catch (RemoteException e) { + Log.e(TAG, "caught exception while sending message to activity >> ", e); + remoteActivityIsAlive = false; + } + } + + private void handleStartRegisterAccount() { + Log.d(TAG, "handleStartRegisterAccount()"); + + Bundle result = new Bundle(); + handleInitializeNotification(); + + handleRegisterAccount(result); + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + + handleImportAccount(result, registerAcount, masterPassphrase); + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + accountWasImported = true; + handleUiCallbackAccountImported(); + + handleImportAddressbook(); + handleDeleteDefaultCalendars(result); + } + else + handleRevertRegisterAccount(result); + } + + Message message = Message.obtain(); + message.arg1 = result.getInt(ErrorToaster.KEY_STATUS_CODE); + resultCode = result.getInt(ErrorToaster.KEY_STATUS_CODE); + + try { + + if (!accountWasImported) + messenger.send(message); + + } catch (RemoteException e) { + Log.e(TAG, "caught exception while sending message to activity >> ", e); + remoteActivityIsAlive = false; + } + + handleImportComplete(); + } + + @Override + public void onCreate() { + HandlerThread thread = new HandlerThread("RegisterAccountService", HandlerThread.NORM_PRIORITY); + thread.start(); + + serviceLooper = thread.getLooper(); + serviceHandler = new ServiceHandler(serviceLooper); + + notifyManager = (NotificationManager)getBaseContext().getSystemService(Context.NOTIFICATION_SERVICE); + notificationBuilder = new NotificationCompat.Builder(getBaseContext()); + } + + private final class ServiceHandler extends Handler { + + public ServiceHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + Log.d(TAG, "handleMessage()"); + Intent intent = msg.getData().getParcelable(KEY_INTENT); + + if (intent != null) { + if (intent.getExtras() != null && + intent.getExtras().get(KEY_MESSENGER) != null && + intent.getExtras().getString(KEY_ACCOUNT_ID) != null && + intent.getExtras().getString(KEY_MASTER_PASSPHRASE) != null) + { + messenger = (Messenger) intent.getExtras().get(KEY_MESSENGER); + accountId = intent.getExtras().getString(KEY_ACCOUNT_ID); + masterPassphrase = intent.getExtras().getString(KEY_MASTER_PASSPHRASE); + + handleStartRegisterAccount(); + } + else + Log.e(TAG, "received intent without messenger, account id or master passphrase"); + } + else + Log.e(TAG, "received message with null intent"); + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Message msg = serviceHandler.obtainMessage(); + msg.getData().putParcelable(KEY_INTENT, intent); + serviceHandler.sendMessage(msg); + + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + Log.d(TAG, "onBind()"); + return null; + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/RemoteAddressbookListAdapter.java b/flock/src/main/java/org/anhonesteffort/flock/RemoteAddressbookListAdapter.java new file mode 100644 index 0000000..a2a47d5 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/RemoteAddressbookListAdapter.java @@ -0,0 +1,47 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.content.Context; + +import org.anhonesteffort.flock.sync.addressbook.HidingCardDavCollection; +import org.anhonesteffort.flock.sync.addressbook.LocalAddressbookStore; + +import java.util.List; + +/** + * Programmer: rhodey + */ +public class RemoteAddressbookListAdapter extends AbstractDavCollectionArrayAdapter { + + public RemoteAddressbookListAdapter(Context context, + HidingCardDavCollection[] remoteAddressbooks, + LocalAddressbookStore localStore, + List selectedAddressbooks) + { + super(context, R.layout.row_remote_addressbook_details, remoteAddressbooks, localStore, selectedAddressbooks); + } + + @Override + protected void handlePopulateView(int position, ViewHolder viewHolder) { + // nothing to do... + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/RemoteCalendarListAdapter.java b/flock/src/main/java/org/anhonesteffort/flock/RemoteCalendarListAdapter.java new file mode 100644 index 0000000..9bfd7ac --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/RemoteCalendarListAdapter.java @@ -0,0 +1,159 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.content.Context; +import android.os.RemoteException; +import android.view.View; +import android.widget.CompoundButton; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.crypto.InvalidMacException; +import org.anhonesteffort.flock.sync.calendar.CalendarsSyncScheduler; +import org.anhonesteffort.flock.sync.calendar.HidingCalDavCollection; +import org.anhonesteffort.flock.sync.calendar.LocalCalendarStore; +import org.anhonesteffort.flock.webdav.PropertyParseException; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.List; + +/** + * Programmer: rhodey + */ +public class RemoteCalendarListAdapter extends AbstractDavCollectionArrayAdapter { + + private View.OnClickListener colorClickListener; + + public RemoteCalendarListAdapter(Context context, + HidingCalDavCollection[] remoteCalendars, + LocalCalendarStore localStore, + List selectedCalendars, + View.OnClickListener colorClickListener) + { + super(context, R.layout.row_remote_calendar_details, remoteCalendars, localStore, selectedCalendars); + this.colorClickListener = colorClickListener; + } + + protected class CalendarViewHolder extends ViewHolder { + + public CalendarViewHolder(ViewHolder viewHolder) { + super(viewHolder); + } + + public View colorView; + } + + @Override + protected ViewHolder getViewHolder(View collectionRowView) { + CalendarViewHolder viewHolder = new CalendarViewHolder(super.getViewHolder(collectionRowView)); + viewHolder.colorView = collectionRowView.findViewById(R.id.calendar_color); + + return viewHolder; + } + + @Override + protected void handlePopulateView(int position, ViewHolder viewHolder) { + CalendarViewHolder calendarViewHolder = (CalendarViewHolder) viewHolder; + + calendarViewHolder.colorView.setOnClickListener(colorClickListener); + calendarViewHolder.colorView.setTag(R.integer.tag_collection_path, remoteCollections[position].getPath()); + + try { + + Optional color = remoteCollections[position].getHiddenColor(); + + if (color.isPresent()) { + calendarViewHolder.colorView.setBackgroundColor(color.get()); + calendarViewHolder.colorView.setTag(R.integer.tag_calendar_color, color.get()); + } + else { + calendarViewHolder.colorView.setBackgroundResource(R.color.flocktheme_color); + calendarViewHolder.colorView.setTag(R.integer.tag_calendar_color, R.color.flocktheme_color); + } + + } catch (PropertyParseException e) { + ErrorToaster.handleShowError(getContext(), e); + } catch (InvalidMacException e) { + ErrorToaster.handleShowError(getContext(), e); + } catch (GeneralSecurityException e) { + ErrorToaster.handleShowError(getContext(), e); + } catch (IOException e) { + ErrorToaster.handleShowError(getContext(), e); + } + } + + @Override + protected CompoundButton.OnCheckedChangeListener getOnCheckChangedListener(HidingCalDavCollection remoteCollection) { + return new SyncChangeListener(remoteCollection); + } + + private class SyncChangeListener implements CompoundButton.OnCheckedChangeListener { + + private HidingCalDavCollection remoteCollection; + private LocalCalendarStore localCalendarStore; + + public SyncChangeListener(HidingCalDavCollection remoteCollection) { + this.remoteCollection = remoteCollection; + this.localCalendarStore = (LocalCalendarStore) localStore; + } + + @Override + public void onCheckedChanged(CompoundButton checkBoxView, boolean isChecked) { + if (!checkBoxView.isShown()) + return; + + try { + + if (isChecked) { + Optional displayName = remoteCollection.getHiddenDisplayName(); + Optional color = remoteCollection.getHiddenColor(); + + if (displayName.isPresent() && color.isPresent()) { + localCalendarStore.addCollection(remoteCollection.getPath(), + displayName.get(), + color.get()); + } + else if (displayName.isPresent()) + localStore.addCollection(remoteCollection.getPath(), displayName.get()); + else { + localStore.addCollection(remoteCollection.getPath(), + getContext().getString(R.string.display_name_missing)); + } + } + else + localStore.removeCollection(remoteCollection.getPath()); + + new CalendarsSyncScheduler(getContext()).requestSync(); + + } catch (PropertyParseException e) { + ErrorToaster.handleShowError(getContext(), e); + } catch (InvalidMacException e) { + ErrorToaster.handleShowError(getContext(), e); + } catch (GeneralSecurityException e) { + ErrorToaster.handleShowError(getContext(), e); + } catch (RemoteException e) { + ErrorToaster.handleShowError(getContext(), e); + } catch (IOException e) { + ErrorToaster.handleShowError(getContext(), e); + } + } + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/SelectServiceProviderFragment.java b/flock/src/main/java/org/anhonesteffort/flock/SelectServiceProviderFragment.java new file mode 100644 index 0000000..d0e27eb --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/SelectServiceProviderFragment.java @@ -0,0 +1,188 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CompoundButton; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.TextView; + +/** + * Programmer: rhodey + */ +public class SelectServiceProviderFragment extends Fragment { + + private AlertDialog alertDialog; + private SetupActivity fragmentActivity; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + if (activity instanceof SetupActivity) + this.fragmentActivity = (SetupActivity) activity; + else + throw new ClassCastException(activity.toString() + " not what I expected D: !"); + } + + @Override + public View onCreateView(LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_select_sync_provider, container, false); + initButtons(); + initRadioButtons(view); + initCostPerYear(view); + + return view; + } + + @Override + public void onPause() { + super.onPause(); + + if (alertDialog != null) + alertDialog.dismiss(); + } + + private void handlePromptNewOrExistingAccount() { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + + builder.setTitle(R.string.title_setup_account); + builder.setMessage(R.string.do_you_have_a_flock_account); + builder.setPositiveButton(R.string.yes_log_me_in, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + fragmentActivity.setIsNewAccount(false); + fragmentActivity.setServiceProvider(SetupActivity.SERVICE_PROVIDER_OWS); + fragmentActivity.updateFragmentUsingState(SetupActivity.STATE_CONFIGURE_SERVICE_PROVIDER); + } + + }); + builder.setNegativeButton(R.string.no_register_me, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialogInterface, int i) { + fragmentActivity.setIsNewAccount(true); + fragmentActivity.setServiceProvider(SetupActivity.SERVICE_PROVIDER_OWS); + fragmentActivity.updateFragmentUsingState(SetupActivity.STATE_CONFIGURE_SERVICE_PROVIDER); + } + + }); + + alertDialog = builder.show(); + } + + private void initButtons() { + getActivity().findViewById(R.id.button_next).setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + RadioButton radioButtonOws = (RadioButton) getActivity().findViewById(R.id.radio_button_service_ows); + + if (radioButtonOws.isChecked()) + handlePromptNewOrExistingAccount(); + + else { + fragmentActivity.setServiceProvider(SetupActivity.SERVICE_PROVIDER_OTHER); + fragmentActivity.updateFragmentUsingState(SetupActivity.STATE_TEST_SERVICE_PROVIDER); + } + } + + }); + } + + private void initRadioButtons(View fragmentView) { + final LinearLayout rowSelectOws = (LinearLayout) fragmentView.findViewById(R.id.row_service_ows); + final LinearLayout rowSelectOther = (LinearLayout) fragmentView.findViewById(R.id.row_service_other); + final RadioButton radioButtonOws = (RadioButton) fragmentView.findViewById(R.id.radio_button_service_ows); + final RadioButton radioButtonOther = (RadioButton) fragmentView.findViewById(R.id.radio_button_service_other); + final TextView serviceDescription = (TextView) fragmentView.findViewById(R.id.sync_service_description); + final Double costPerYearUsd = (double) getResources().getInteger(R.integer.cost_per_year_usd); + + rowSelectOws.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View view) { + if (!radioButtonOws.isChecked()) { + radioButtonOws.setChecked(true); + radioButtonOther.setChecked(false); + serviceDescription.setText(getString(R.string.flock_sync_is_a_service_run_by_open_whisper_systems_providing, costPerYearUsd)); + } + } + + }); + + rowSelectOther.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View view) { + if (!radioButtonOther.isChecked()) { + radioButtonOther.setChecked(true); + radioButtonOws.setChecked(false); + serviceDescription.setText(R.string.you_may_chose_to_run_and_configure_your_own_webdav_compliant_server); + } + } + + }); + + radioButtonOws.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { + if (isChecked) { + radioButtonOws.setChecked(true); + radioButtonOther.setChecked(false); + serviceDescription.setText(getString(R.string.flock_sync_is_a_service_run_by_open_whisper_systems_providing, costPerYearUsd)); + } + } + + }); + + radioButtonOther.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { + if (isChecked) { + radioButtonOther.setChecked(true); + radioButtonOws.setChecked(false); + serviceDescription.setText(R.string.you_may_chose_to_run_and_configure_your_own_webdav_compliant_server); + } + } + + }); + } + + private void initCostPerYear(View fragmentView) { + final TextView serviceDescription = (TextView) fragmentView.findViewById(R.id.sync_service_description); + final Double costPerYearUsd = (double) getResources().getInteger(R.integer.cost_per_year_usd); + serviceDescription.setText(getString(R.string.flock_sync_is_a_service_run_by_open_whisper_systems_providing, costPerYearUsd)); + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/SendBitcoinActivity.java b/flock/src/main/java/org/anhonesteffort/flock/SendBitcoinActivity.java new file mode 100644 index 0000000..b6b52c2 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/SendBitcoinActivity.java @@ -0,0 +1,486 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.util.Log; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.Window; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.common.base.Optional; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.MultiFormatWriter; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; +import com.stripe.exception.StripeException; +import com.stripe.model.Receiver; +import de.passsy.holocircularprogressbar.HoloCircularProgressBar; + +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.registration.OwsRegistration; +import org.anhonesteffort.flock.registration.model.FlockAccount; + +import java.text.DecimalFormat; +import java.util.Date; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + +/** + * Programmer: rhodey + */ +public class SendBitcoinActivity extends Activity { + + private static final String TAG = "org.anhonesteffort.flock.SendBitcoinActivity"; + + public static final String KEY_DAV_ACCOUNT_BUNDLE = "KEY_DAV_ACCOUNT_BUNDLE"; + + private final Handler uiHandler = new Handler(); + private Timer intervalTimer = new Timer(); + private AsyncTask asyncTaskBtc; + + private DavAccount davAccount; + private Optional btcReceiver = Optional.absent(); + private Optional qrCodeBitmap = Optional.absent(); + private Optional lastBtcUri = Optional.absent(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + requestWindowFeature(Window.FEATURE_PROGRESS); + + setContentView(R.layout.activity_send_bitcoin); + getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setTitle(R.string.title_send_bitcoin); + + if (savedInstanceState != null && !savedInstanceState.isEmpty()) { + if (!DavAccount.build(savedInstanceState.getBundle(KEY_DAV_ACCOUNT_BUNDLE)).isPresent()) { + finish(); + return; + } + + davAccount = DavAccount.build(savedInstanceState.getBundle(KEY_DAV_ACCOUNT_BUNDLE)).get(); + } + else if (getIntent().getExtras() != null) { + if (!DavAccount.build(getIntent().getExtras().getBundle(KEY_DAV_ACCOUNT_BUNDLE)).isPresent()) { + finish(); + return; + } + + davAccount = DavAccount.build(getIntent().getExtras().getBundle(KEY_DAV_ACCOUNT_BUNDLE)).get(); + } + + initButtons(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + break; + } + + return false; + } + + @Override + public void onResume() { + super.onResume(); + + handleStartPerpetualRefresh(); + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + savedInstanceState.putBundle(KEY_DAV_ACCOUNT_BUNDLE, davAccount.toBundle()); + super.onSaveInstanceState(savedInstanceState); + } + + @Override + public void onPause() { + super.onPause(); + + if (asyncTaskBtc != null && !asyncTaskBtc.isCancelled()) + asyncTaskBtc.cancel(true); + + if (intervalTimer != null) + intervalTimer.cancel(); + } + + private boolean handleLaunchBtcWallet(String btcURI) { + Log.d(TAG, "handleLaunchBtcWallet()"); + + Intent btcWalletIntent = new Intent(Intent.ACTION_VIEW); + btcWalletIntent.setData(Uri.parse(btcURI)); + + try { + + startActivity(btcWalletIntent); + Log.d(TAG, "I think something was actually launched"); + + } catch (ActivityNotFoundException e) { + return false; + } + + return true; + } + + private void initButtons() { + findViewById(R.id.button_cancel).setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + finish(); + } + + }); + + findViewById(R.id.layout_btc_address).setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (!btcReceiver.isPresent()) + return false; + + if (event.getAction() == MotionEvent.ACTION_DOWN && + !handleLaunchBtcWallet(btcReceiver.get().getBitcoinUri())) + { + TextView addressBtcView = (TextView)findViewById(R.id.btc_address); + View triangleView = findViewById(R.id.btc_address_triangle); + + addressBtcView.setBackgroundResource(R.drawable.rounded_thing_orange); + triangleView.setBackgroundResource(R.drawable.conversation_item_received_triangle_shape_orange); + + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("oioi", btcReceiver.get().getInboundAddress()); + + clipboard.setPrimaryClip(clip); + Toast.makeText(getBaseContext(), + R.string.address_copied_to_clipboard, + Toast.LENGTH_SHORT).show(); + } + + else if (event.getAction() == MotionEvent.ACTION_UP) { + TextView addressBtcView = (TextView) findViewById(R.id.btc_address); + View triangleView = findViewById(R.id.btc_address_triangle); + + addressBtcView.setBackgroundResource(R.drawable.rounded_thing_grey); + triangleView.setBackgroundResource(R.drawable.conversation_item_received_triangle_shape_grey); + } + + return true; + } + }); + + findViewById(R.id.layout_btc_address).setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + if (!btcReceiver.isPresent()) + return; + + TextView addressBtcView = (TextView)findViewById(R.id.btc_address); + addressBtcView.setBackgroundResource(R.drawable.rounded_thing_orange); + + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("oioi", btcReceiver.get().getInboundAddress()); + + clipboard.setPrimaryClip(clip); + Toast.makeText(getBaseContext(), + R.string.address_copied_to_clipboard, + Toast.LENGTH_SHORT).show(); + } + + }); + } + + private void handleUpdateUi() { + ImageView qrCodeView = (ImageView)findViewById(R.id.image_btc_qr_code); + TextView microBtcLargeView = (TextView)findViewById(R.id.mbtc_due_large); + TextView microBtcSmallView = (TextView)findViewById(R.id.mbtc_due_small); + TextView addressBtcView = (TextView)findViewById(R.id.btc_address); + TextView minutesRemainingView = (TextView)findViewById(R.id.btc_minutes_remaining); + HoloCircularProgressBar progressBarView = (HoloCircularProgressBar)findViewById(R.id.btc_address_progress); + + if (!btcReceiver.isPresent()) + return; + + if (!qrCodeBitmap.isPresent() || + (lastBtcUri.isPresent() && + !lastBtcUri.get().equals(btcReceiver.get().getBitcoinUri()))) + { + try { + + qrCodeBitmap = encodeAsBitmap(btcReceiver.get().getBitcoinUri(), + BarcodeFormat.AZTEC, + qrCodeView.getWidth(), + qrCodeView.getHeight()); + + } catch (WriterException e) { + Log.e(TAG, "caught exception while encoding BTC URI", e); + } + + if (qrCodeBitmap.isPresent()) + qrCodeView.setImageBitmap(qrCodeBitmap.get()); + } + else + qrCodeView.setImageBitmap(qrCodeBitmap.get()); + + lastBtcUri = Optional.of(btcReceiver.get().getBitcoinUri()); + + Double amountMBtc = btcReceiver.get().getBitcoinAmount() / 100000000.0 * 1000.0; + int decimal_index = amountMBtc.toString().indexOf("."); + String amountMBtcRemainder = null; + + if (decimal_index + 3 > amountMBtc.toString().length()) + amountMBtcRemainder = "0"; + else if ((amountMBtc.toString().length() - decimal_index) > 6) + amountMBtcRemainder = amountMBtc.toString().substring(decimal_index + 3, decimal_index + 9); + else + amountMBtcRemainder = amountMBtc.toString().substring(decimal_index + 3); + + microBtcLargeView.setText(new DecimalFormat("0.00").format(amountMBtc)); + microBtcSmallView.setText(amountMBtcRemainder); + + String addressBtcFormatted = ""; + for(int i = 0; i < btcReceiver.get().getInboundAddress().length(); i++) { + if (i % 4 == 0 && i != 0) + addressBtcFormatted += " "; + addressBtcFormatted += btcReceiver.get().getInboundAddress().charAt(i); + } + + addressBtcView.setText(addressBtcFormatted); + + long secondsRemaining = ((btcReceiver.get().getCreated() * 1000) - new Date().getTime()) / 1000 + ((10 * 60) - 1); + Integer minutesRemaining = (int) (secondsRemaining / 60); + + if ((secondsRemaining - (minutesRemaining * 60)) < 0) { + minutesRemainingView.setText("0:00"); + progressBarView.setProgress(1.0F); + } + else if ((secondsRemaining - (minutesRemaining * 60)) < 10) { + minutesRemainingView.setText(minutesRemaining + ":0" + (secondsRemaining - (minutesRemaining * 60))); + progressBarView.setProgress(1.0F - ((float) secondsRemaining / (10 * 60))); + } + else { + minutesRemainingView.setText(minutesRemaining + ":" + (secondsRemaining - (minutesRemaining * 60))); + progressBarView.setProgress(1.0F - ((float) secondsRemaining / (10 * 60))); + } + } + + private void handleRefreshBitcoinReceiver() { + asyncTaskBtc = new AsyncTask() { + + private Receiver receiver; + + @Override + protected void onPreExecute() { + Log.d(TAG, "handleRefreshBitcoinReceiver()"); + setProgressBarIndeterminateVisibility(true); + setProgressBarVisibility(true); + } + + private void handleExpireStoredBtcReceiver() { + String KEY_ID_BTC_RECEIVER = "KEY_ID_BTC_RECEIVER"; + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); + String btcReceiverId = preferences.getString(KEY_ID_BTC_RECEIVER, null); + + if (btcReceiverId != null) + preferences.edit().remove(KEY_ID_BTC_RECEIVER).commit(); + } + + private Receiver handleCreateNewBtcReceiver(Double costUsd) throws StripeException { + Map receiverParams = new HashMap(); + + receiverParams.put("currency", "usd"); + receiverParams.put("amount", (int) (costUsd * 100)); + receiverParams.put("description", "Flock Subscription"); + receiverParams.put("email", davAccount.getUserId()); + + return Receiver.create(receiverParams, OwsRegistration.STRIPE_PUBLIC_KEY); + } + + private Receiver getOrCreateNewBtcReceiver(Double costUsd) throws StripeException { + Receiver btcReceiver = null; + String KEY_ID_BTC_RECEIVER = "KEY_ID_BTC_RECEIVER"; + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); + String btcReceiverId = preferences.getString(KEY_ID_BTC_RECEIVER, null); + + if (btcReceiverId != null) { + btcReceiver = Receiver.retrieve(btcReceiverId, OwsRegistration.STRIPE_PUBLIC_KEY); + Long timeCreated = btcReceiver.getCreated() * 1000; + + if ((timeCreated + (10 * 60 * 1000)) < new Date().getTime()) { + Log.d(TAG, "btc receiver expired, creating new"); + btcReceiverId = null; + } + } + + if (btcReceiverId == null) { + btcReceiver = handleCreateNewBtcReceiver(costUsd); + preferences.edit().putString(KEY_ID_BTC_RECEIVER, btcReceiver.getId()).commit(); + return btcReceiver; + } + + return Receiver.retrieve(btcReceiverId, OwsRegistration.STRIPE_PUBLIC_KEY); + } + + @Override + protected Bundle doInBackground(String... params) { + Bundle result = new Bundle(); + Double costPerYearUsd = (double) getResources().getInteger(R.integer.cost_per_year_usd); + + try { + + receiver = getOrCreateNewBtcReceiver(costPerYearUsd); + + } catch (StripeException e) { + Log.e(TAG, "stripe is mad", e); + ErrorToaster.handleBundleError(e, result); + } + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + Log.d(TAG, "STATUS: " + result.getInt(ErrorToaster.KEY_STATUS_CODE)); + + asyncTaskBtc = null; + setProgressBarIndeterminateVisibility(false); + setProgressBarVisibility(false); + + if (receiver != null) { + btcReceiver = Optional.of(receiver); + handleUpdateUi(); + + if (receiver.getFilled()) { + handleExpireStoredBtcReceiver(); + Toast.makeText(getBaseContext(), + R.string.bitcoin_received, + Toast.LENGTH_SHORT).show(); + finish(); + } + } + else + ErrorToaster.handleDisplayToastBundledError(getBaseContext(), result); + } + }.execute(); + } + + private final Runnable refreshUiRunnable = new Runnable() { + + @Override + public void run() { + handleUpdateUi(); + } + + }; + private final Runnable refreshBtcRunnable = new Runnable() { + + @Override + public void run() { + if (asyncTaskBtc == null || asyncTaskBtc.isCancelled()) + handleRefreshBitcoinReceiver(); + } + + }; + + private void handleStartPerpetualRefresh() { + intervalTimer = new Timer(); + TimerTask uiTask = new TimerTask() { + + @Override + public void run() { + uiHandler.post(refreshUiRunnable); + } + + }; + TimerTask btcTask = new TimerTask() { + + @Override + public void run() { + uiHandler.post(refreshBtcRunnable); + } + + }; + + intervalTimer.schedule(uiTask, 0, 1000); + intervalTimer.schedule(btcTask, 0, 10000); + } + + Optional encodeAsBitmap(String dataToEncode, + BarcodeFormat format, + int bitmap_width, + int bitmap_height) + throws WriterException + { + Map hints = new EnumMap(EncodeHintType.class); + hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); + + MultiFormatWriter writer = new MultiFormatWriter(); + BitMatrix result; + + try { + + result = writer.encode(dataToEncode, format, bitmap_width, bitmap_height, hints); + + } catch (IllegalArgumentException e) { + Log.e(TAG, "caught exception while attempting to encode " + + dataToEncode + " as bitmap", e); + return Optional.absent(); + } + + + int[] pixel_array = new int[result.getWidth() * result.getHeight()]; + for (int y = 0; y < result.getHeight(); y++) { + for (int x = 0; x < result.getWidth(); x++) + pixel_array[(y * result.getWidth()) + x] = result.get(x, y) ? 0xFF000000 : 0xFFFFFFFF; + } + + Bitmap bitmap = Bitmap.createBitmap(result.getWidth(), result.getHeight(), Bitmap.Config.ARGB_8888); + bitmap.setPixels(pixel_array, 0, result.getWidth(), 0, 0, result.getWidth(), result.getHeight()); + + return Optional.of(bitmap); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/ServerTestsFragment.java b/flock/src/main/java/org/anhonesteffort/flock/ServerTestsFragment.java new file mode 100644 index 0000000..1a1b5c0 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/ServerTestsFragment.java @@ -0,0 +1,798 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.app.Activity; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.common.base.Optional; +import ezvcard.VCard; +import ezvcard.VCardVersion; +import ezvcard.property.StructuredName; +import ezvcard.property.Uid; +import net.fortuna.ical4j.model.ConstraintViolationException; +import net.fortuna.ical4j.model.Date; +import net.fortuna.ical4j.model.component.VEvent; +import net.fortuna.ical4j.model.property.CalScale; +import net.fortuna.ical4j.model.property.Description; +import net.fortuna.ical4j.model.property.Version; +import net.fortuna.ical4j.util.Calendars; +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.sync.OwsWebDav; +import org.anhonesteffort.flock.webdav.ComponentETagPair; +import org.anhonesteffort.flock.webdav.InvalidComponentException; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.anhonesteffort.flock.webdav.caldav.CalDavCollection; +import org.anhonesteffort.flock.webdav.caldav.CalDavStore; +import org.anhonesteffort.flock.webdav.carddav.CardDavCollection; +import org.anhonesteffort.flock.webdav.carddav.CardDavStore; +import org.apache.commons.lang.StringUtils; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; + +import java.io.IOException; +import java.util.Calendar; +import java.util.List; +import java.util.UUID; +import javax.net.ssl.SSLException; + +/** + * Programmer: rhodey + */ +public class ServerTestsFragment extends Fragment { + + private static final String TAG = "org.anhonesteffort.flock.ServerTestsFragment"; + + private static final String KEY_HREF_DAV_HOST = "KEY_HREF_DAV_HOST"; + private static final String KEY_DAV_USERNAME = "KEY_DAV_USERNAME"; + + private static final int CODE_ERROR_CARDDAV_CURRENT_USER_PRINCIPAL = 100; + private static final int CODE_ERROR_CALDAV_CURRENT_USER_PRINCIPAL = 101; + private static final int CODE_ERROR_CALDAV_CALENDAR_HOMESET = 102; + private static final int CODE_ERROR_CARDDAV_ADDRESSBOOK_HOMESET = 103; + private static final int CODE_ERROR_CALDAV_CREATE_DELETE_COLLECTION = 104; + private static final int CODE_ERROR_CALDAV_CREATE_EDIT_COLLECTION_PROPERTIES = 105; + private static final int CODE_ERROR_CARDDAV_CREATE_DELETE_CONTACTS = 106; + private static final int CODE_ERROR_CALDAV_CREATE_DELETE_EVENTS = 107; + + private SetupActivity setupActivity; + private AsyncTask asyncTask; + + private Optional hrefDavHost = Optional.absent(); + private Optional davUsername = Optional.absent(); + + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (savedInstanceState != null) { + hrefDavHost = Optional.fromNullable(savedInstanceState.getString(KEY_HREF_DAV_HOST)); + davUsername = Optional.fromNullable(savedInstanceState.getString(KEY_DAV_USERNAME)); + } + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + super.onSaveInstanceState(savedInstanceState); + + TextView davHostView = (TextView)getView().findViewById(R.id.href_webdav_host); + TextView davUsernameView = (TextView)getView().findViewById(R.id.account_username); + + + if (davHostView.getText() != null) + savedInstanceState.putString(KEY_HREF_DAV_HOST, davHostView.getText().toString()); + + if (davUsernameView.getText() != null) + savedInstanceState.putString(KEY_DAV_USERNAME, davUsernameView.getText().toString()); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + if (activity instanceof SetupActivity) + this.setupActivity = (SetupActivity) activity; + else + throw new ClassCastException(activity.toString() + " not what I expected D: !"); + } + + @Override + public View onCreateView(LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_server_tests, container, false); + + initButtons(); + initForm(); + + return view; + } + + @Override + public void onPause() { + super.onPause(); + + if (asyncTask != null && !asyncTask.isCancelled()) + asyncTask.cancel(true); + } + + private void initButtons() { + getActivity().findViewById(R.id.button_next).setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + handleStartTests(); + } + + }); + } + + private void initForm() { + TextView davHostView = (TextView)getActivity().findViewById(R.id.href_webdav_host); + TextView davUsernameView = (TextView)getActivity().findViewById(R.id.account_username); + + if (hrefDavHost.isPresent()) + davHostView.setText(hrefDavHost.get()); + + if (davUsername.isPresent()) + davUsernameView.setText(davUsername.get()); + } + + private void handleTestsSucceeded() { + Log.d(TAG, "handleTestsSucceeded()"); + + String webDavHost = ((TextView)getView().findViewById(R.id.href_webdav_host)).getText().toString().trim(); + String username = ((TextView)getView().findViewById(R.id.account_username)).getText().toString().trim(); + + Toast.makeText(getActivity(), + R.string.tests_completed_successfully, + Toast.LENGTH_SHORT).show(); + + if (StringUtils.isNotEmpty(webDavHost) && StringUtils.isNotEmpty(username)) + setupActivity.setDavTestOptions(webDavHost, username); + + setupActivity.updateFragmentUsingState(SetupActivity.STATE_CONFIGURE_SERVICE_PROVIDER); + } + + private void handleStartTests() { + Log.d(TAG, "handleStartTests()"); + + if (asyncTask == null || asyncTask.isCancelled()) { + ((Button) getActivity().findViewById(R.id.button_next)).setText(R.string.stop_tests); + startTests(); + } + else { + ((Button) getActivity().findViewById(R.id.button_next)).setText(R.string.restart_tests); + asyncTask.cancel(true); + asyncTask = null; + } + } + + private void startTests() { + + asyncTask = new AsyncTask() { + + private TextView currentTest; + private ImageView testErrorImage; + private ProgressBar progressBar; + private int progress = 0; + + @Override + protected void onPreExecute() { + Log.d(TAG, "startTests()"); + + currentTest = (TextView) getView().findViewById(R.id.text_current_test); + testErrorImage = (ImageView) getView().findViewById(R.id.image_current_test_failed); + progressBar = (ProgressBar) getView().findViewById(R.id.progress_server_tests); + + currentTest.setText(R.string.tests_not_yet_started); + testErrorImage.setVisibility(View.GONE); + + progressBar.setMax(9); + progressBar.setProgress(0); + } + + private void handleCardDavTestCurrentUserPrincipal(Bundle result, DavAccount testAccount) { + try { + + CardDavStore cardDavStore = DavAccountHelper.getCardDavStore(getActivity(), testAccount); + Optional currentUserPrincipal = cardDavStore.getCurrentUserPrincipal(); + + if (currentUserPrincipal.isPresent()) + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + else + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CARDDAV_CURRENT_USER_PRINCIPAL); + + } catch (DavException e) { + + if (e.getErrorCode() == DavServletResponse.SC_UNAUTHORIZED) + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_UNAUTHORIZED); + else + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CARDDAV_CURRENT_USER_PRINCIPAL); + + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + } + + private void handleCalDavTestCurrentUserPrincipal(Bundle result, DavAccount testAccount) { + try { + + CalDavStore calDavStore = DavAccountHelper.getCalDavStore(getActivity(), testAccount); + Optional currentUserPrincipal = calDavStore.getCurrentUserPrincipal(); + + if (currentUserPrincipal.isPresent()) + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + else + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CURRENT_USER_PRINCIPAL); + + } catch (DavException e) { + + Log.d(TAG, e.toString()); + if (e.getErrorCode() == DavServletResponse.SC_UNAUTHORIZED) + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_UNAUTHORIZED); + else + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CURRENT_USER_PRINCIPAL); + + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + } + + private void handleCardDavTestAddressbookHomeset(Bundle result, DavAccount testAccount) { + try { + + CardDavStore cardDavStore = DavAccountHelper.getCardDavStore(getActivity(), testAccount); + Optional addressbookHomeset = cardDavStore.getAddressbookHomeSet(); + + if (addressbookHomeset.isPresent()) + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + else + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CARDDAV_ADDRESSBOOK_HOMESET); + + } catch (DavException e) { + Log.d(TAG, e.toString()); + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CARDDAV_ADDRESSBOOK_HOMESET); + } catch (PropertyParseException e) { + Log.d(TAG, e.toString()); + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CARDDAV_ADDRESSBOOK_HOMESET); + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + } + + private void handleCalDavTestCalendarHomeset(Bundle result, DavAccount testAccount) { + try { + + CalDavStore calDavStore = DavAccountHelper.getCalDavStore(getActivity(), testAccount); + Optional calendarHomeset = calDavStore.getCalendarHomeSet(); + + if (calendarHomeset.isPresent()) + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + else + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CALENDAR_HOMESET); + + } catch (DavException e) { + Log.d(TAG, e.toString()); + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CALENDAR_HOMESET); + } catch (PropertyParseException e) { + Log.d(TAG, e.toString()); + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CALENDAR_HOMESET); + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + } + + private void handleCalDavTestCreateDeleteCollection(Bundle result, DavAccount testAccount) { + try { + + CalDavStore calDavStore = DavAccountHelper.getCalDavStore(getActivity(), testAccount); + Optional calendarHomeset = calDavStore.getCalendarHomeSet(); + + if (!calendarHomeset.isPresent()) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CALENDAR_HOMESET); + return; + } + + String tempCollectionPath = calendarHomeset.get().concat("delete-me/"); + + calDavStore.addCollection(tempCollectionPath); + Optional tempCollection = calDavStore.getCollection(tempCollectionPath); + + if (!tempCollection.isPresent()) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CREATE_DELETE_COLLECTION); + return; + } + + calDavStore.removeCollection(tempCollectionPath); + tempCollection = calDavStore.getCollection(tempCollectionPath); + + if (tempCollection.isPresent()) + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CREATE_DELETE_COLLECTION); + else + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (DavException e) { + Log.d(TAG, e.toString()); + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CREATE_DELETE_COLLECTION); + } catch (PropertyParseException e) { + Log.d(TAG, e.toString()); + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CREATE_DELETE_COLLECTION); + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + } + + private void handleCalDavTestCreateEditCollectionProperties(Bundle result, + DavAccount testAccount) + { + final String TEMP_DISPLAY_NAME = "TEMP DISPLAY NAME"; + + try { + + CalDavStore calDavStore = DavAccountHelper.getCalDavStore(getActivity(), testAccount); + Optional calendarHomeset = calDavStore.getCalendarHomeSet(); + + if (!calendarHomeset.isPresent()) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CALENDAR_HOMESET); + return; + } + + String tempCollectionPath = calendarHomeset.get().concat("delete-me/"); + + calDavStore.addCollection(tempCollectionPath); + Optional tempCollection = calDavStore.getCollection(tempCollectionPath); + + if (!tempCollection.isPresent()) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CREATE_DELETE_COLLECTION); + return; + } + + tempCollection.get().setDisplayName(TEMP_DISPLAY_NAME); + + if (!tempCollection.get().getDisplayName().isPresent() || + !tempCollection.get().getDisplayName().get().equals(TEMP_DISPLAY_NAME)) + { + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CREATE_EDIT_COLLECTION_PROPERTIES); + return; + } + + final DavPropertyName TEST_PROP_NAME = DavPropertyName.create("X-TEST-XPROPERTIES", OwsWebDav.NAMESPACE); + final String TEST_PROP_VALUE = "TEST PROPERTY VALUE"; + + DavPropertySet setProps = new DavPropertySet(); + DavPropertyNameSet fetchPropNames = new DavPropertyNameSet(); + + setProps.add(new DefaultDavProperty(TEST_PROP_NAME, TEST_PROP_VALUE)); + fetchPropNames.add(TEST_PROP_NAME); + + tempCollection.get().patchProperties(setProps, new DavPropertyNameSet()); + tempCollection.get().fetchProperties(fetchPropNames); + + Optional gotTestProp = tempCollection.get().getProperty(TEST_PROP_NAME, String.class); + + if (!gotTestProp.isPresent() || !gotTestProp.get().equals(TEST_PROP_VALUE)) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CREATE_EDIT_COLLECTION_PROPERTIES); + return; + } + + calDavStore.removeCollection(tempCollectionPath); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (DavException e) { + Log.d(TAG, e.toString()); + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CREATE_EDIT_COLLECTION_PROPERTIES); + } catch (PropertyParseException e) { + Log.d(TAG, e.toString()); + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CREATE_EDIT_COLLECTION_PROPERTIES); + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + } + + private void handleCardDavTestCreateDeleteContacts(Bundle result, + DavAccount testAccount) + { + try { + + CardDavStore cardDavStore = DavAccountHelper.getCardDavStore(getActivity(), testAccount); + List collections = cardDavStore.getCollections(); + + if (collections.size() == 0) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CARDDAV_CREATE_DELETE_CONTACTS); + return; + } + + CardDavCollection testCollection = collections.get(0); + + final StructuredName structuredName = new StructuredName(); + structuredName.setFamily("Strangelove"); + structuredName.setGiven("?"); + structuredName.addPrefix("Dr"); + structuredName.addSuffix(""); + + VCard putVCard = new VCard(); + putVCard.setVersion(VCardVersion.V3_0); + putVCard.setUid(new Uid(UUID.randomUUID().toString())); + putVCard.setStructuredName(structuredName); + putVCard.setFormattedName("you need this too"); + + final String EXTENDED_PROPERTY_NAME = "X-EXTENDED-PROPERTY-NAME"; + final String EXTENDED_PROPERTY_VALUE = "THIS IS A LINE LONG ENOUGH TO BE SPLIT IN TWO BY THE VCARD FOLDING NONSENSE WHY DOES THIS EXIST?!?!??!?!?!?!?!??"; + putVCard.setExtendedProperty(EXTENDED_PROPERTY_NAME, EXTENDED_PROPERTY_VALUE); + + testCollection.addComponent(putVCard); + + Optional> gotVCard = testCollection.getComponent(putVCard.getUid().getValue()); + + if (!gotVCard.isPresent()) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CARDDAV_CREATE_DELETE_CONTACTS); + return; + } + + if (!gotVCard.get().getComponent().getStructuredName().getFamily().equals(structuredName.getFamily())) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CARDDAV_CREATE_DELETE_CONTACTS); + return; + } + + if (gotVCard.get().getComponent().getExtendedProperty(EXTENDED_PROPERTY_NAME) == null || + !gotVCard.get().getComponent().getExtendedProperty(EXTENDED_PROPERTY_NAME).getValue().equals(EXTENDED_PROPERTY_VALUE)) + { + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CARDDAV_CREATE_DELETE_CONTACTS); + return; + } + + + testCollection.removeComponent(putVCard.getUid().getValue()); + + if (testCollection.getComponent(putVCard.getUid().getValue()).isPresent()) + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CARDDAV_CREATE_DELETE_CONTACTS); + else + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (DavException e) { + Log.d(TAG, e.toString()); + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CARDDAV_CREATE_DELETE_CONTACTS); + } catch (PropertyParseException e) { + Log.d(TAG, e.toString()); + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CARDDAV_CREATE_DELETE_CONTACTS); + } catch (InvalidComponentException e) { + Log.d(TAG, e.toString()); + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CARDDAV_CREATE_DELETE_CONTACTS); + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + } + + private void handleCalDavTestCreateDeleteEvents(Bundle result, DavAccount testAccount) { + try { + + CalDavStore calDavStore = DavAccountHelper.getCalDavStore(getActivity(), testAccount); + Optional calendarHomeset = calDavStore.getCalendarHomeSet(); + + if (!calendarHomeset.isPresent()) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CALENDAR_HOMESET); + return; + } + + String tempCollectionPath = calendarHomeset.get().concat("delete-me/"); + + calDavStore.addCollection(tempCollectionPath); + Optional tempCollection = calDavStore.getCollection(tempCollectionPath); + + if (!tempCollection.isPresent()) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CREATE_DELETE_COLLECTION); + return; + } + + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.MONTH, Calendar.JUNE); + calendar.set(Calendar.DAY_OF_MONTH, 5); + + net.fortuna.ical4j.model.Calendar putCalendar = new net.fortuna.ical4j.model.Calendar(); + putCalendar.getProperties().add(Version.VERSION_2_0); + putCalendar.getProperties().add(CalScale.GREGORIAN); + + Date putStartDate = new Date(calendar.getTime()); + Date putEndDate = new Date(putStartDate.getTime() + (1000 * 60 * 60 * 24)); + + VEvent vEventPut = new VEvent(putStartDate, putEndDate, "Delete Me!"); + vEventPut.getProperties().add(new net.fortuna.ical4j.model.property.Uid(UUID.randomUUID().toString())); + vEventPut.getProperties().add(new Description("THIS IS A LINE LONG ENOUGH TO BE SPLIT IN TWO BY THE ICAL FOLDING NONSENSE WHY DOES THIS EXIST?!?!??!?!?!?!?!??")); + putCalendar.getComponents().add(vEventPut); + + tempCollection.get().addComponent(putCalendar); + + Optional> gotCalendar = + tempCollection.get().getComponent(Calendars.getUid(putCalendar).getValue()); + + if (!gotCalendar.isPresent()) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CREATE_DELETE_EVENTS); + return; + } + + VEvent vEventGot = (VEvent) gotCalendar.get().getComponent().getComponent(VEvent.VEVENT); + + if (vEventGot == null || + !vEventGot.getSummary().getValue().equals(vEventPut.getSummary().getValue())) + { + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CREATE_DELETE_EVENTS); + return; + } + + tempCollection.get().removeComponent(Calendars.getUid(putCalendar).getValue()); + + gotCalendar = tempCollection.get().getComponent(Calendars.getUid(putCalendar).getValue()); + + if (gotCalendar.isPresent()) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CREATE_DELETE_EVENTS); + return; + } + + calDavStore.removeCollection(tempCollectionPath); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (DavException e) { + Log.d(TAG, e.toString()); + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CREATE_DELETE_EVENTS); + } catch (PropertyParseException e) { + Log.d(TAG, e.toString()); + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CREATE_DELETE_EVENTS); + } catch (InvalidComponentException e) { + Log.d(TAG, e.toString()); + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CREATE_DELETE_EVENTS); + } catch (ConstraintViolationException e) { + Log.d(TAG, e.toString()); + result.putInt(ErrorToaster.KEY_STATUS_CODE, CODE_ERROR_CALDAV_CREATE_DELETE_EVENTS); + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + } + + @Override + protected Bundle doInBackground(String... params) { + String webDavHost = ((TextView)getView().findViewById(R.id.href_webdav_host)).getText().toString().trim(); + String username = ((TextView)getView().findViewById(R.id.account_username)).getText().toString().trim(); + String password = ((TextView)getView().findViewById(R.id.account_password)).getText().toString().trim(); + Bundle result = new Bundle(); + + if (StringUtils.isEmpty(username)) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_EMPTY_ACCOUNT_ID); + return result; + } + + if (StringUtils.isEmpty(password)) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SHORT_PASSWORD); + return result; + } + + DavAccount testAccount = new DavAccount(username, password, webDavHost); + + progress++; + publishProgress(); + handleCardDavTestCurrentUserPrincipal(result, testAccount); + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + progress++; + publishProgress(); + handleCalDavTestCurrentUserPrincipal(result, testAccount); + } + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + progress++; + publishProgress(); + handleCardDavTestAddressbookHomeset(result, testAccount); + } + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + progress++; + publishProgress(); + handleCalDavTestCalendarHomeset(result, testAccount); + } + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + progress++; + publishProgress(); + handleCalDavTestCreateDeleteCollection(result, testAccount); + } + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + progress++; + publishProgress(); + handleCalDavTestCreateEditCollectionProperties(result, testAccount); + } + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + progress++; + publishProgress(); + handleCardDavTestCreateDeleteContacts(result, testAccount); + } + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + progress++; + publishProgress(); + handleCalDavTestCreateDeleteEvents(result, testAccount); + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) { + progress++; + publishProgress(); + } + } + + return result; + } + + @Override + protected void onProgressUpdate(final Void... values) { + progressBar.setProgress(progress); + + switch (progress) { + + case 0: + currentTest.setText(R.string.tests_not_yet_started); + break; + + case 1: + currentTest.setText(R.string.test_dav_current_user_principal); + break; + + case 2: + currentTest.setText(R.string.test_dav_current_user_principal); + break; + + case 3: + currentTest.setText(R.string.test_carddav_addressbook_homeset); + break; + + case 4: + currentTest.setText(R.string.test_caldav_calendar_homeset); + break; + + case 5: + currentTest.setText(R.string.test_caldav_create_and_delete_collections); + break; + + case 6: + currentTest.setText(R.string.test_caldav_create_and_edit_collection_properties); + break; + + case 7: + currentTest.setText(R.string.test_carddav_create_and_delete_contacts); + break; + + case 8: + currentTest.setText(R.string.test_caldav_create_and_delete_events); + break; + + } + } + + @Override + protected void onCancelled() { + currentTest.setText(R.string.tests_interrupted); + progressBar.setProgress(0); + } + + @Override + protected void onPostExecute(Bundle result) { + Log.d(TAG, "STATUS: " + result.getInt(ErrorToaster.KEY_STATUS_CODE)); + + asyncTask = null; + testErrorImage.setVisibility(View.VISIBLE); + ((Button)getActivity().findViewById(R.id.button_next)).setText(R.string.restart_tests); + + switch (result.getInt(ErrorToaster.KEY_STATUS_CODE)) { + + case ErrorToaster.CODE_SUCCESS: + testErrorImage.setVisibility(View.GONE); + handleTestsSucceeded(); + break; + + case CODE_ERROR_CARDDAV_CURRENT_USER_PRINCIPAL: + + Toast.makeText(getActivity(), + R.string.test_error_carddav_current_user_principal, + Toast.LENGTH_LONG).show(); + break; + + case CODE_ERROR_CALDAV_CURRENT_USER_PRINCIPAL: + Toast.makeText(getActivity(), + R.string.test_error_carddav_current_user_principal, + Toast.LENGTH_LONG).show(); + break; + + case CODE_ERROR_CARDDAV_ADDRESSBOOK_HOMESET: + Toast.makeText(getActivity(), + R.string.test_error_carddav_addressbook_homeset, + Toast.LENGTH_LONG).show(); + break; + + case CODE_ERROR_CALDAV_CALENDAR_HOMESET: + Toast.makeText(getActivity(), + R.string.test_error_caldav_calendar_homeset, + Toast.LENGTH_LONG).show(); + break; + + case CODE_ERROR_CALDAV_CREATE_DELETE_COLLECTION: + Toast.makeText(getActivity(), + R.string.test_error_caldav_create_and_delete_collections, + Toast.LENGTH_LONG).show(); + break; + + case CODE_ERROR_CALDAV_CREATE_EDIT_COLLECTION_PROPERTIES: + Toast.makeText(getActivity(), + R.string.test_error_caldav_create_and_edit_collection_properties, + Toast.LENGTH_LONG).show(); + break; + + case CODE_ERROR_CARDDAV_CREATE_DELETE_CONTACTS: + Toast.makeText(getActivity(), + R.string.test_error_carddav_create_and_delete_contacts, + Toast.LENGTH_LONG).show(); + break; + + case CODE_ERROR_CALDAV_CREATE_DELETE_EVENTS: + Toast.makeText(getActivity(), + R.string.test_error_caldav_create_and_delete_events, + Toast.LENGTH_LONG).show(); + break; + + default: + ErrorToaster.handleDisplayToastBundledError(getActivity(), result); + + } + + ((TextView)getView().findViewById(R.id.account_password)).setText(""); + } + }.execute(); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/SetupActivity.java b/flock/src/main/java/org/anhonesteffort/flock/SetupActivity.java new file mode 100644 index 0000000..f712809 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/SetupActivity.java @@ -0,0 +1,465 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentTransaction; +import android.view.View; +import android.view.Window; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import com.example.android.wizardpager.wizard.ui.StepPagerStrip; +import com.google.common.base.Optional; +import org.anhonesteffort.flock.sync.key.KeySyncScheduler; + +/** + * Programmer: rhodey + */ +public class SetupActivity extends FragmentActivity { + + protected static final String KEY_SETUP_STATE = "SetupActivity.KEY_MANAGE_STATE"; + protected static final String KEY_NAVIGATION_DISABLED = "SetupActivity.KEY_NAVIGATION_DISABLED"; + protected static final String KEY_SERVICE_PROVIDER = "SetupActivity.KEY_SERVICE_PROVIDER"; + protected static final String KEY_IS_NEW_ACCOUNT = "SetupActivity.KEY_IS_NEW_ACCOUNT"; + protected static final String KEY_DAV_TEST_HOST = "SetupActivity.KEY_DAV_TEST_HOST"; + protected static final String KEY_DAV_TEST_USERNAME = "KEY_DAV_TEST_USERNAME"; + + protected static final int STATE_INTRO = 0; + protected static final int STATE_SELECT_SERVICE_PROVIDER = 1; + protected static final int STATE_TEST_SERVICE_PROVIDER = 2; + protected static final int STATE_CONFIGURE_SERVICE_PROVIDER = 3; + protected static final int STATE_IMPORT_CONTACTS = 4; + protected static final int STATE_IMPORT_CALENDARS = 5; + protected static final int STATE_SELECT_REMOTE_ADDRESSBOOK = 6; + protected static final int STATE_SELECT_REMOTE_CALENDARS = 7; + + protected static final int SERVICE_PROVIDER_OWS = 0; + protected static final int SERVICE_PROVIDER_OTHER = 1; + + private int state = STATE_INTRO; + private boolean navigationDisabled = false; + private Optional serviceProvider = Optional.absent(); + private Optional isNewAccount = Optional.absent(); + private Optional davTestHost = Optional.absent(); + private Optional davTestUsername = Optional.absent(); + + private StepPagerStrip setupStepIndicator; + private TextView setupStepTitle; + private Button buttonPrevious; + private Button buttonNext; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + requestWindowFeature(Window.FEATURE_PROGRESS); + + setContentView(R.layout.setup_activity); + getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setTitle(R.string.app_name); + + setupStepIndicator = (StepPagerStrip) findViewById(R.id.setup_step_indicator); + setupStepTitle = (TextView) findViewById(R.id.setup_activity_large_text); + buttonPrevious = (Button) findViewById(R.id.button_previous); + buttonNext = (Button) findViewById(R.id.button_next); + + buttonPrevious.setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View view) { + handleButtonPrevious(); + } + + }); + } + + protected void setNavigationDisabled(boolean navigationDisabled) { + this.navigationDisabled = navigationDisabled; + } + + @Override + public void onBackPressed() { + handleButtonPrevious(); + } + + private void limitMultipleAccounts() { + if (DavAccountHelper.isAccountRegistered(getBaseContext())) { + Toast.makeText(this, R.string.error_multiple_accounts_not_allowed, Toast.LENGTH_SHORT).show(); + finish(); + } + } + + @Override + protected void onResume() { + super.onResume(); + + if (state == STATE_SELECT_SERVICE_PROVIDER || state == STATE_TEST_SERVICE_PROVIDER || + state == STATE_CONFIGURE_SERVICE_PROVIDER) + { + limitMultipleAccounts(); + } + + updateFragmentUsingState(state); + } + + @Override + public void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + + if (savedInstanceState != null) { + if (savedInstanceState.containsKey(KEY_SETUP_STATE)) + state = savedInstanceState.getInt(KEY_SETUP_STATE); + + if (savedInstanceState.containsKey(KEY_NAVIGATION_DISABLED)) + navigationDisabled = savedInstanceState.getBoolean(KEY_NAVIGATION_DISABLED); + + if (savedInstanceState.containsKey(KEY_SERVICE_PROVIDER)) + serviceProvider = Optional.of(savedInstanceState.getInt(KEY_SERVICE_PROVIDER)); + + if (savedInstanceState.containsKey(KEY_IS_NEW_ACCOUNT)) + isNewAccount = Optional.of(savedInstanceState.getBoolean(KEY_IS_NEW_ACCOUNT)); + + if (savedInstanceState.containsKey(KEY_DAV_TEST_HOST)) + davTestHost = Optional.of(savedInstanceState.getString(KEY_DAV_TEST_HOST)); + + if (savedInstanceState.containsKey(KEY_DAV_TEST_USERNAME)) + davTestUsername = Optional.of(savedInstanceState.getString(KEY_DAV_TEST_USERNAME)); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putInt(KEY_SETUP_STATE, state); + outState.putBoolean(KEY_NAVIGATION_DISABLED, navigationDisabled); + + if (serviceProvider.isPresent()) + outState.putInt(KEY_SERVICE_PROVIDER, serviceProvider.get()); + + if (isNewAccount.isPresent()) + outState.putBoolean(KEY_IS_NEW_ACCOUNT, isNewAccount.get()); + + if (davTestHost.isPresent()) + outState.putString(KEY_DAV_TEST_HOST, davTestHost.get()); + + if (davTestUsername.isPresent()) + outState.putString(KEY_DAV_TEST_USERNAME, davTestUsername.get()); + } + + protected void setServiceProvider(Integer provider) { + this.serviceProvider = Optional.of(provider); + } + + protected void setDavTestOptions(String davTestHost, String davTestUsername) { + this.davTestHost = Optional.of(davTestHost); + this.davTestUsername = Optional.of(davTestUsername); + } + + protected void setIsNewAccount(Boolean isNew) { + this.isNewAccount = Optional.of(isNew); + } + + protected void handleSetupComplete() { + new KeySyncScheduler(getBaseContext()).requestSync(); + Toast.makeText(getBaseContext(), R.string.setup_complete, Toast.LENGTH_LONG).show(); + + Intent nextIntent = new Intent(getBaseContext(), PreferencesActivity.class); + nextIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(nextIntent); + + finish(); + } + + protected void updateFragmentUsingState(int newState) { + FragmentTransaction fragmentTransaction; + Fragment nextFragment; + boolean replaceFragment = (newState != STATE_INTRO && state != newState); + + switch (newState) { + + case STATE_INTRO: + setupStepIndicator.setVisibility(View.GONE); + setupStepTitle.setVisibility(View.GONE); + + fragmentTransaction = getSupportFragmentManager().beginTransaction(); + nextFragment = new IntroductionFragment(); + + fragmentTransaction.replace(R.id.fragment_view, nextFragment); + fragmentTransaction.commit(); + + buttonPrevious.setVisibility(View.INVISIBLE); + buttonNext.setText(R.string.begin); + buttonNext.setVisibility(View.VISIBLE); + break; + + case STATE_SELECT_SERVICE_PROVIDER: + setupStepIndicator.setVisibility(View.GONE); + setupStepIndicator.setCurrentPage(0); + setupStepTitle.setVisibility(View.VISIBLE); + setupStepTitle.setText(R.string.title_chose_sync_service); + + if (replaceFragment) { + fragmentTransaction = getSupportFragmentManager().beginTransaction(); + nextFragment = new SelectServiceProviderFragment(); + + fragmentTransaction.replace(R.id.fragment_view, nextFragment); + fragmentTransaction.commit(); + } + + buttonPrevious.setVisibility(View.VISIBLE); + buttonNext.setText(R.string.next); + buttonNext.setVisibility(View.VISIBLE); + break; + + case STATE_TEST_SERVICE_PROVIDER: + setupStepIndicator.setVisibility(View.VISIBLE); + setupStepIndicator.setPageCount(7); + setupStepIndicator.setCurrentPage(1); + setupStepTitle.setText(R.string.title_server_tests); + + if (replaceFragment) { + fragmentTransaction = getSupportFragmentManager().beginTransaction(); + nextFragment = new ServerTestsFragment(); + + fragmentTransaction.replace(R.id.fragment_view, nextFragment); + fragmentTransaction.commit(); + } + + buttonPrevious.setVisibility(View.VISIBLE); + buttonNext.setText(R.string.next); + buttonNext.setVisibility(View.VISIBLE); + break; + + case STATE_CONFIGURE_SERVICE_PROVIDER: + setupStepIndicator.setVisibility(View.VISIBLE); + setupStepTitle.setText(R.string.title_server_tests); + + if (serviceProvider.isPresent() && serviceProvider.get().equals(SERVICE_PROVIDER_OTHER)) { + setupStepIndicator.setPageCount(7); + setupStepIndicator.setCurrentPage(2); + setupStepTitle.setText(R.string.title_import_account); + + ImportOtherAccountFragment hack = new ImportOtherAccountFragment(); + if (davTestHost.isPresent() && davTestUsername.isPresent()) + hack.setDavTestOptions(davTestHost.get(), davTestUsername.get()); + + nextFragment = hack; + } + else if (isNewAccount.isPresent() && isNewAccount.get()) { + setupStepIndicator.setPageCount(4); + setupStepIndicator.setCurrentPage(1); + setupStepTitle.setText(R.string.title_register_account); + + nextFragment = new RegisterAccountFragment(); + } + else { + setupStepIndicator.setPageCount(2); + setupStepIndicator.setCurrentPage(1); + setupStepTitle.setText(R.string.title_import_account); + + nextFragment = new ImportOwsAccountFragment(); + } + + if (replaceFragment) { + fragmentTransaction = getSupportFragmentManager().beginTransaction(); + fragmentTransaction.replace(R.id.fragment_view, nextFragment); + fragmentTransaction.commit(); + } + + buttonPrevious.setVisibility(View.VISIBLE); + buttonNext.setText(R.string.next); + buttonNext.setVisibility(View.VISIBLE); + break; + + case STATE_IMPORT_CONTACTS: + if (serviceProvider.isPresent() && serviceProvider.get().equals(SERVICE_PROVIDER_OWS) && + isNewAccount.isPresent() && !isNewAccount.get()) + { + handleSetupComplete(); + break; + } + + if (serviceProvider.isPresent() && !serviceProvider.get().equals(SERVICE_PROVIDER_OWS)) { + setupStepIndicator.setPageCount(7); + setupStepIndicator.setCurrentPage(3); + } + else { + setupStepIndicator.setPageCount(4); + setupStepIndicator.setCurrentPage(2); + } + + setupStepTitle.setText(R.string.title_import_contacts); + + Toast.makeText(getBaseContext(), + R.string.select_accounts_to_import_contacts_from, + Toast.LENGTH_LONG).show(); + + if (replaceFragment) { + fragmentTransaction = getSupportFragmentManager().beginTransaction(); + nextFragment = new ImportContactsFragment(); + + fragmentTransaction.replace(R.id.fragment_view, nextFragment); + fragmentTransaction.commit(); + } + + buttonPrevious.setVisibility(View.INVISIBLE); + buttonNext.setText(R.string.next); + buttonNext.setVisibility(View.VISIBLE); + break; + + case STATE_IMPORT_CALENDARS: + if (serviceProvider.isPresent() && !serviceProvider.get().equals(SERVICE_PROVIDER_OWS)) { + setupStepIndicator.setPageCount(7); + setupStepIndicator.setCurrentPage(4); + } + else { + setupStepIndicator.setPageCount(4); + setupStepIndicator.setCurrentPage(3); + } + setupStepTitle.setText(R.string.title_import_calendars); + + Toast.makeText(getBaseContext(), + R.string.select_calendars_to_import, + Toast.LENGTH_SHORT).show(); + + if (replaceFragment) { + fragmentTransaction = getSupportFragmentManager().beginTransaction(); + nextFragment = new ImportCalendarsFragment(); + + fragmentTransaction.replace(R.id.fragment_view, nextFragment); + fragmentTransaction.commit(); + } + + buttonPrevious.setVisibility(View.VISIBLE); + buttonNext.setText(R.string.next); + buttonNext.setVisibility(View.VISIBLE); + break; + + case STATE_SELECT_REMOTE_ADDRESSBOOK: + if (serviceProvider.isPresent() && serviceProvider.get().equals(SERVICE_PROVIDER_OWS) && + isNewAccount.isPresent() && isNewAccount.get()) + { + handleSetupComplete(); + break; + } + + setupStepIndicator.setPageCount(7); + setupStepIndicator.setCurrentPage(5); + setupStepTitle.setText(R.string.title_my_addressbooks); + + Toast.makeText(getBaseContext(), + R.string.select_a_single_remote_addressbook_in_which_to_store, + Toast.LENGTH_LONG).show(); + + if (replaceFragment) { + fragmentTransaction = getSupportFragmentManager().beginTransaction(); + nextFragment = new MyAddressbooksFragment(); + + fragmentTransaction.replace(R.id.fragment_view, nextFragment); + fragmentTransaction.commit(); + } + + buttonPrevious.setVisibility(View.VISIBLE); + buttonNext.setText(R.string.next); + buttonNext.setVisibility(View.VISIBLE); + break; + + case STATE_SELECT_REMOTE_CALENDARS: + setupStepIndicator.setPageCount(7); + setupStepIndicator.setCurrentPage(6); + setupStepTitle.setText(R.string.title_my_calendars); + + Toast.makeText(getBaseContext(), + R.string.select_the_calendars_you_would_like_to_sync_with_this_device, + Toast.LENGTH_LONG).show(); + + if (replaceFragment) { + fragmentTransaction = getSupportFragmentManager().beginTransaction(); + nextFragment = new MyCalendarsFragment(); + + fragmentTransaction.replace(R.id.fragment_view, nextFragment); + fragmentTransaction.commit(); + } + + buttonPrevious.setVisibility(View.VISIBLE); + buttonNext.setText(R.string.next); + buttonNext.setVisibility(View.VISIBLE); + break; + } + + state = newState; + } + + private void handleButtonPrevious() { + if (navigationDisabled) + return; + + setupStepTitle.setTextColor(0xff0099cc); + buttonNext.setBackgroundResource(R.drawable.selectable_item_background); + buttonNext.setText(R.string.next); + + switch (state) { + + case STATE_INTRO: + finish(); + break; + + case STATE_SELECT_SERVICE_PROVIDER: + updateFragmentUsingState(STATE_INTRO); + break; + + case STATE_TEST_SERVICE_PROVIDER: + updateFragmentUsingState(STATE_SELECT_SERVICE_PROVIDER); + break; + + case STATE_CONFIGURE_SERVICE_PROVIDER: + if (serviceProvider.isPresent() && serviceProvider.get().equals(SERVICE_PROVIDER_OTHER)) + updateFragmentUsingState(STATE_TEST_SERVICE_PROVIDER); + else + updateFragmentUsingState(STATE_SELECT_SERVICE_PROVIDER); + break; + + case STATE_IMPORT_CONTACTS: + updateFragmentUsingState(STATE_IMPORT_CONTACTS); + break; + + case STATE_IMPORT_CALENDARS: + updateFragmentUsingState(STATE_IMPORT_CONTACTS); + break; + + case STATE_SELECT_REMOTE_ADDRESSBOOK: + updateFragmentUsingState(STATE_IMPORT_CALENDARS); + break; + + case STATE_SELECT_REMOTE_CALENDARS: + updateFragmentUsingState(STATE_SELECT_REMOTE_ADDRESSBOOK); + break; + + } + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/StatusHeaderView.java b/flock/src/main/java/org/anhonesteffort/flock/StatusHeaderView.java new file mode 100644 index 0000000..fa1f828 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/StatusHeaderView.java @@ -0,0 +1,397 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.content.ContentResolver; +import android.content.Context; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.view.LayoutInflater; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.crypto.KeyHelper; +import org.anhonesteffort.flock.registration.RegistrationApi; +import org.anhonesteffort.flock.registration.RegistrationApiException; +import org.anhonesteffort.flock.sync.AbstractDavSyncAdapter; +import org.anhonesteffort.flock.sync.addressbook.AddressbookSyncScheduler; +import org.anhonesteffort.flock.sync.calendar.CalendarsSyncScheduler; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.jackrabbit.webdav.DavException; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.text.DateFormat; +import java.util.Date; +import java.util.Timer; +import java.util.TimerTask; + +public class StatusHeaderView extends LinearLayout { + + private final static String TAG = StatusHeaderView.class.getSimpleName(); + + private final Handler uiHandler = new Handler(); + private Timer intervalTimer = new Timer(); + + private final TextView timeLastSyncView; + private final TextView syncStatusView; + + private Optional account; + private AsyncTask asyncTaskSubscription; + private AsyncTask asyncTaskCard; + private AsyncTask asyncTaskMasterPassphrase; + + private long timeLastSync = -1; + private boolean syncInProgress = false; + private boolean subscriptionIsValid = true; + private boolean cardIsValid = true; + private boolean cipherPassphraseIsValid = true; + private boolean authNotificationShown = false; + private boolean subscriptionNotificationShown = false; + + private boolean syncServerHasError = false; + private boolean registrationServerHasError = false; + private boolean syncServerErrorNotificationShown = false; + private boolean registrationServerErrorNotificationShown = false; + + public StatusHeaderView(Context context) { + super(context); + + LayoutInflater.from(context).inflate(R.layout.status_header_view, this); + + account = DavAccountHelper.getAccount(context); + timeLastSyncView = (TextView) getRootView().findViewById(R.id.last_sync_time); + syncStatusView = (TextView) getRootView().findViewById(R.id.sync_status); + } + + public void hackOnPause() { + if (asyncTaskSubscription != null && !asyncTaskSubscription.isCancelled()) + asyncTaskSubscription.cancel(true); + + if (asyncTaskCard != null && !asyncTaskCard.isCancelled()) + asyncTaskCard.cancel(true); + + if (asyncTaskMasterPassphrase != null && !asyncTaskMasterPassphrase.isCancelled()) + asyncTaskMasterPassphrase.cancel(true); + + if (intervalTimer != null) + intervalTimer.cancel(); + } + + private void handleUpdateTimeLastSync() { + AddressbookSyncScheduler addressbookSync = new AddressbookSyncScheduler(getContext()); + CalendarsSyncScheduler calendarSync = new CalendarsSyncScheduler(getContext()); + + Optional lastContactSync = addressbookSync.getTimeLastSync(); + Optional lastCalendarSync = calendarSync.getTimeLastSync(); + + if (lastCalendarSync.isPresent() && !lastContactSync.isPresent()) + timeLastSync = lastCalendarSync.get(); + else if (!lastCalendarSync.isPresent() && lastContactSync.isPresent()) + timeLastSync = lastContactSync.get(); + else if (!lastCalendarSync.isPresent() && !lastContactSync.isPresent()) + timeLastSync = -1; + else + timeLastSync = Math.min(lastContactSync.get(), lastCalendarSync.get()); + + if (!account.isPresent()) + return; + + syncInProgress = addressbookSync.syncInProgress(account.get().getOsAccount()) || + calendarSync.syncInProgress(account.get().getOsAccount()); + } + + private void handleUpdateLayout() { + final String timeLastSyncText; + final String syncStatusText; + final int syncStatusDrawable; + + if (timeLastSync == -1) { + timeLastSyncView.setVisibility(GONE); + syncStatusView.setText(getContext().getString(R.string.status_header_sync_in_progress)); + syncStatusView.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.sync_in_progress, 0, 0); + invalidate(); + return; + } + else { + DateFormat formatter = DateFormat.getDateTimeInstance(); + timeLastSyncText = formatter.format(new Date(timeLastSync)); + timeLastSyncView.setText(getContext().getString(R.string.status_header_sync_time, timeLastSyncText)); + timeLastSyncView.setVisibility(VISIBLE); + } + + if (syncServerHasError) { + if (account.isPresent() && DavAccountHelper.isUsingOurServers(account.get())) + syncStatusText = getContext().getString(R.string.status_header_status_our_sync_service_error); + else + syncStatusText = getContext().getString(R.string.status_header_status_their_sync_service_error); + + syncStatusDrawable = R.drawable.sad_cloud; + } + else if (registrationServerHasError) { + syncStatusText = getContext().getString(R.string.status_header_status_registration_service_error); + syncStatusDrawable = R.drawable.sad_cloud; + } + else if (!DavAccountHelper.getAccountPassword(getContext()).isPresent()) { + syncStatusText = getContext().getString(R.string.status_header_status_account_login_failed); + syncStatusDrawable = R.drawable.sad_cloud; + if (!authNotificationShown) { + AbstractDavSyncAdapter.showAuthNotificationAndInvalidatePassword(getContext()); + authNotificationShown = true; + } + } + else if (!cipherPassphraseIsValid) { + syncStatusText = getContext().getString(R.string.status_header_status_encryption_password_incorrect); + syncStatusDrawable = R.drawable.sad_cloud; + } + else if (!subscriptionIsValid) { + syncStatusText = getContext().getString(R.string.notification_flock_subscription_expired); + syncStatusDrawable = R.drawable.sad_cloud; + if (!subscriptionNotificationShown) { + AbstractDavSyncAdapter.showSubscriptionExpiredNotification(getContext()); + subscriptionNotificationShown = true; + } + } + else if (!cardIsValid) { + syncStatusText = getContext().getString(R.string.status_header_status_auto_renew_error); + syncStatusDrawable = R.drawable.sad_cloud; + } + else if (!ContentResolver.getMasterSyncAutomatically()) { + syncStatusText = getContext().getString(R.string.status_header_status_sync_disabled); + syncStatusDrawable = R.drawable.sad_cloud; + } + else if (syncInProgress) { + syncStatusText = getContext().getString(R.string.status_header_sync_in_progress); + syncStatusDrawable = R.drawable.sync_in_progress; + } + else { + syncStatusText = getContext().getString(R.string.status_header_status_good); + syncStatusDrawable = R.drawable.happy_cloud; + } + + syncStatusView.setText(syncStatusText); + syncStatusView.setCompoundDrawablesWithIntrinsicBounds(0, syncStatusDrawable, 0, 0); + invalidate(); + } + + private void handleUpdateSubscriptionIsValid() { + if (!account.isPresent() || (asyncTaskSubscription != null && !asyncTaskSubscription.isCancelled())) + return; + + asyncTaskSubscription = new AsyncTask() { + + boolean subscriptionExpired = false; + + @Override + protected Bundle doInBackground(String... params) { + Bundle result = new Bundle(); + + try { + + subscriptionExpired = DavAccountHelper.isExpired(getContext(), account.get()); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } catch (PropertyParseException e) { + ErrorToaster.handleBundleError(e, result); + } catch (DavException e) { + ErrorToaster.handleBundleError(e, result); + } + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + asyncTaskSubscription = null; + subscriptionIsValid = !subscriptionExpired; + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_DAV_SERVER_ERROR) { + if (!syncServerErrorNotificationShown || !syncServerHasError) { + ErrorToaster.handleDisplayToastBundledError(getContext(), result); + syncServerHasError = true; + syncServerErrorNotificationShown = true; + } + } + else if (result.getInt(ErrorToaster.KEY_STATUS_CODE) != ErrorToaster.CODE_SUCCESS) + ErrorToaster.handleDisplayToastBundledError(getContext(), result); + } + + }.execute(); + } + + private void handleUpdateCardIsValid() { + if (!account.isPresent() || (asyncTaskCard != null && !asyncTaskCard.isCancelled())) + return; + + asyncTaskCard = new AsyncTask() { + + boolean lastChargeFailed = false; + + @Override + protected Bundle doInBackground(String... params) { + Bundle result = new Bundle(); + RegistrationApi registrationApi = new RegistrationApi(getContext()); + + try { + + if (registrationApi.getAccount(account.get()).getLastStripeChargeFailed()) + lastChargeFailed = registrationApi.getCard(account.get()).isPresent(); + + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } catch (RegistrationApiException e) { + ErrorToaster.handleBundleError(e, result); + } + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + asyncTaskCard = null; + cardIsValid = !lastChargeFailed; + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_REGISTRATION_API_SERVER_ERROR) { + if (!registrationServerErrorNotificationShown || !registrationServerHasError) { + ErrorToaster.handleDisplayToastBundledError(getContext(), result); + registrationServerHasError = true; + registrationServerErrorNotificationShown = true; + } + } + else if (result.getInt(ErrorToaster.KEY_STATUS_CODE) != ErrorToaster.CODE_SUCCESS) + ErrorToaster.handleDisplayToastBundledError(getContext(), result); + } + + }.execute(); + } + + private void handleUpdateCipherPassphraseIsValid() { + if (asyncTaskMasterPassphrase != null && !asyncTaskMasterPassphrase.isCancelled()) + return; + + asyncTaskMasterPassphrase = new AsyncTask() { + + boolean passphraseIsValid = false; + + @Override + protected Bundle doInBackground(String... params) { + Bundle result = new Bundle(); + + try { + + passphraseIsValid = KeyHelper.masterPassphraseIsValid(getContext()); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } catch (GeneralSecurityException e) { + ErrorToaster.handleBundleError(e, result); + } + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + asyncTaskMasterPassphrase = null; + cipherPassphraseIsValid = passphraseIsValid; + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) != ErrorToaster.CODE_SUCCESS) + ErrorToaster.handleDisplayToastBundledError(getContext(), result); + } + + }.execute(); + } + + private final Runnable refreshUiRunnable = new Runnable() { + @Override + public void run() { + handleUpdateTimeLastSync(); + handleUpdateLayout(); + } + }; + private final Runnable refreshSubscriptionRunnable = new Runnable() { + @Override + public void run() { + handleUpdateSubscriptionIsValid(); + } + }; + private final Runnable refreshCardRunnable = new Runnable() { + @Override + public void run() { + handleUpdateCardIsValid(); + } + }; + private final Runnable refreshCipherPassphraseRunnable = new Runnable() { + @Override + public void run() { + handleUpdateCipherPassphraseIsValid(); + } + }; + + public void handleStartPerpetualRefresh() { + account = DavAccountHelper.getAccount(getContext()); + intervalTimer = new Timer(); + + TimerTask uiTask = new TimerTask() { + @Override + public void run() { + uiHandler.post(refreshUiRunnable); + } + }; + TimerTask subscriptionTask = new TimerTask() { + @Override + public void run() { + uiHandler.post(refreshSubscriptionRunnable); + } + }; + TimerTask cardTask = new TimerTask() { + @Override + public void run() { + uiHandler.post(refreshCardRunnable); + } + }; + TimerTask passphraseTask = new TimerTask() { + @Override + public void run() { + uiHandler.post(refreshCipherPassphraseRunnable); + } + }; + + intervalTimer.schedule(uiTask, 0, 2000); + + if (account.isPresent()) { + if (DavAccountHelper.isUsingOurServers(account.get())) { + intervalTimer.schedule(subscriptionTask, 0, 20000); + intervalTimer.schedule(cardTask, 0, 20000); + } + else + intervalTimer.schedule(passphraseTask, 0, 10000); + } + } +} \ No newline at end of file diff --git a/flock/src/main/java/org/anhonesteffort/flock/UnregisterAccountActivity.java b/flock/src/main/java/org/anhonesteffort/flock/UnregisterAccountActivity.java new file mode 100644 index 0000000..18902bd --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/UnregisterAccountActivity.java @@ -0,0 +1,255 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock; + +import android.accounts.AccountManager; +import android.accounts.AccountManagerFuture; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.util.Log; +import android.view.MenuItem; +import android.view.View; +import android.view.Window; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.auth.AccountAuthenticator; +import org.anhonesteffort.flock.crypto.KeyStore; +import org.anhonesteffort.flock.registration.RegistrationApi; +import org.anhonesteffort.flock.registration.RegistrationApiException; +import org.anhonesteffort.flock.sync.addressbook.AddressbookSyncScheduler; +import org.anhonesteffort.flock.sync.addressbook.LocalAddressbookStore; +import org.anhonesteffort.flock.sync.addressbook.LocalContactCollection; +import org.anhonesteffort.flock.sync.calendar.CalendarsSyncScheduler; +import org.anhonesteffort.flock.sync.key.KeySyncScheduler; + +import java.io.IOException; +import java.util.List; +import javax.net.ssl.SSLException; + +/** + * Programmer: rhodey + */ +public class UnregisterAccountActivity extends AccountAndKeyRequiredActivity { + + private static final String TAG = "org.anhonesteffort.flock.DeleteAccountActivity"; + + private AlertDialog alertDialog; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (!accountAndKeyAvailable()) + return; + + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + requestWindowFeature(Window.FEATURE_PROGRESS); + + setContentView(R.layout.unregister_account); + getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setTitle(R.string.title_unregister_account); + + initButtons(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + break; + } + + return false; + } + + private void initButtons() { + findViewById(R.id.button_delete_account).setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + EditText cipherPassphrase = (EditText) findViewById(R.id.cipher_passphrase); + + Optional storedPassphrase = KeyStore.getMasterPassphrase(getBaseContext()); + if (cipherPassphrase.getText() != null && + !cipherPassphrase.getText().toString().equals(storedPassphrase.get())) + { + Toast.makeText(getBaseContext(), + R.string.error_invalid_encryption_password, + Toast.LENGTH_LONG).show(); + } + else + handleRemoveAccount(); + } + + }); + } + + @Override + public void onPause() { + super.onPause(); + Log.d(TAG, "onPause()"); + + if (alertDialog != null) + alertDialog.dismiss(); + } + + private void handleAccountRemoved() { + Log.d(TAG, "handleAccountRemoved()"); + + new KeySyncScheduler(getBaseContext()).onAccountRemoved(); + new AddressbookSyncScheduler(getBaseContext()).onAccountRemoved(); + new CalendarsSyncScheduler(getBaseContext()).onAccountRemoved(); + + Toast.makeText(getBaseContext(), + R.string.account_has_been_unregistered, + Toast.LENGTH_SHORT).show(); + + finish(); + } + + private void handleRemoveAccount() { + Log.d(TAG, "handleRemoveAccount()"); + + AlertDialog.Builder builder = new AlertDialog.Builder(UnregisterAccountActivity.this); + + builder.setTitle(R.string.title_unregister_account); + builder.setMessage(R.string.are_you_sure_you_want_to_unregister_your_account); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int id) { + removeAccountAsync(); + } + + }); + + alertDialog = builder.show(); + } + + private void removeAccountAsync() { + new AsyncTask() { + + @Override + protected void onPreExecute() { + Log.d(TAG, "removeAccountAsync()"); + setProgressBarIndeterminateVisibility(true); + setProgressBarVisibility(true); + } + + private void handleAddressbookCleanup() { + LocalAddressbookStore store = new LocalAddressbookStore(getBaseContext(), account); + List collections = store.getCollections(); + + for (LocalContactCollection collection : collections) + collection.setCTag(null); + + account.setCardDavCollection(getBaseContext(), null); + } + + private void handleRemoveAccountFromDevice(Bundle result) { + Log.d(TAG, "handleRemoveAccountFromDevice()"); + + AccountAuthenticator.setAllowAccountRemoval(getBaseContext(), true); + AccountManagerFuture future = + AccountManager.get(getBaseContext()).removeAccount(account.getOsAccount(), null, null); + + try { + + if (!future.getResult()) { + Log.e(TAG, "I don't know what android did"); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_ACCOUNT_MANAGER_ERROR); + } + + else { + + handleAddressbookCleanup(); + DavAccountHelper.invalidateAccount(getBaseContext()); + KeyStore.invalidateKeyMaterial(getBaseContext()); + PreferenceManager.getDefaultSharedPreferences(getBaseContext()).edit().clear().commit(); + + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + } + + } catch (Exception e) { + Log.e(TAG, "I don't know what android did", e); + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_ACCOUNT_MANAGER_ERROR); + } + } + + private void handleRemoveAccountFromServer(Bundle result) { + try { + + RegistrationApi registrationApi = new RegistrationApi(getBaseContext()); + registrationApi.deleteAccount(account); + + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_SUCCESS); + + } catch (RegistrationApiException e) { + ErrorToaster.handleBundleError(e, result); + } catch (SSLException e) { + ErrorToaster.handleBundleError(e, result); + } catch (IOException e) { + ErrorToaster.handleBundleError(e, result); + } + } + + @Override + protected Bundle doInBackground(String... params) { + Bundle result = new Bundle(); + String cipherPassphrase = ((TextView)findViewById(R.id.cipher_passphrase)).getText().toString().trim(); + + Optional storedPassphrase = KeyStore.getMasterPassphrase(getBaseContext()); + if (!cipherPassphrase.equals(storedPassphrase.get())) { + result.putInt(ErrorToaster.KEY_STATUS_CODE, ErrorToaster.CODE_INVALID_CIPHER_PASSPHRASE); + return result; + } + + handleRemoveAccountFromServer(result); + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) + handleRemoveAccountFromDevice(result); + + return result; + } + + @Override + protected void onPostExecute(Bundle result) { + Log.d(TAG, "STATUS: " + result.getInt(ErrorToaster.KEY_STATUS_CODE)); + setProgressBarIndeterminateVisibility(false); + setProgressBarVisibility(false); + + if (result.getInt(ErrorToaster.KEY_STATUS_CODE) == ErrorToaster.CODE_SUCCESS) + handleAccountRemoved(); + else + ErrorToaster.handleDisplayToastBundledError(getBaseContext(), result); + + ((TextView)findViewById(R.id.cipher_passphrase)).setText(""); + } + }.execute(); + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/auth/AccountAuthenticator.java b/flock/src/main/java/org/anhonesteffort/flock/auth/AccountAuthenticator.java new file mode 100644 index 0000000..4702147 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/auth/AccountAuthenticator.java @@ -0,0 +1,136 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.auth; + +/** + * Programmer: rhodey + */ + +import android.accounts.AbstractAccountAuthenticator; +import android.accounts.Account; +import android.accounts.AccountAuthenticatorResponse; +import android.accounts.AccountManager; +import android.accounts.NetworkErrorException; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; + +import org.anhonesteffort.flock.R; +import org.anhonesteffort.flock.SetupActivity; + + +public class AccountAuthenticator extends AbstractAccountAuthenticator { + + private static final String PREFERENCES_NAME = "AccountAuthenticator-Preferences"; + public static final String KEY_ALLOW_ACCOUNT_REMOVAL = "AccountAuthenticator.KEY_ALLOW_ACCOUNT_REMOVAL"; + + private Context context; + + public AccountAuthenticator(Context context) { + super(context); + this.context = context; + } + + @Override + public String getAuthTokenLabel(String authTokenType) { + return context.getString(R.string.app_name); + } + + public static void setAllowAccountRemoval(Context context, boolean isAllowed) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, + Context.MODE_MULTI_PROCESS); + settings.edit().putBoolean(KEY_ALLOW_ACCOUNT_REMOVAL, isAllowed).commit(); + } + + @Override + public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response, Account account) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, + Context.MODE_MULTI_PROCESS); + Boolean isAllowed = settings.getBoolean(KEY_ALLOW_ACCOUNT_REMOVAL, false); + + Bundle resultBundle = new Bundle(); + resultBundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, isAllowed); + return resultBundle; + } + + @Override + public Bundle addAccount(AccountAuthenticatorResponse response, + String accountType, + String authTokenType, + String[] requiredFeatures, + Bundle options) + throws NetworkErrorException + { + Bundle promptUserBundle = new Bundle(); + Intent promptUserIntent = new Intent(context, SetupActivity.class); + + promptUserIntent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); + promptUserBundle.putParcelable(AccountManager.KEY_INTENT, promptUserIntent); + return promptUserBundle; + } + + @Override + public Bundle getAuthToken(AccountAuthenticatorResponse response, + Account account, + String authTokenType, + Bundle options) + throws NetworkErrorException + { + throw new UnsupportedOperationException(); + } + + @Override + public Bundle updateCredentials(AccountAuthenticatorResponse response, + Account account, + String authTokenType, + Bundle options) + throws NetworkErrorException + { + throw new UnsupportedOperationException(); + } + + @Override + public Bundle hasFeatures(AccountAuthenticatorResponse response, + Account account, + String[] features) + throws NetworkErrorException + { + Bundle result = new Bundle(); + result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false); + return result; + } + + @Override + public Bundle confirmCredentials(AccountAuthenticatorResponse response, + Account account, + Bundle options) + { + throw new UnsupportedOperationException(); + } + + @Override + public Bundle editProperties(AccountAuthenticatorResponse response, + String accountType) + { + throw new UnsupportedOperationException(); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/auth/AccountAuthenticatorService.java b/flock/src/main/java/org/anhonesteffort/flock/auth/AccountAuthenticatorService.java new file mode 100644 index 0000000..408042b --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/auth/AccountAuthenticatorService.java @@ -0,0 +1,37 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.auth; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +/** + * Programmer: rhodey + */ +public class AccountAuthenticatorService extends Service { + + @Override + public IBinder onBind(Intent intent) { + AccountAuthenticator authenticator = new AccountAuthenticator(this); + return authenticator.getIBinder(); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/auth/DavAccount.java b/flock/src/main/java/org/anhonesteffort/flock/auth/DavAccount.java new file mode 100644 index 0000000..d193cbd --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/auth/DavAccount.java @@ -0,0 +1,110 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.auth; + +import android.accounts.Account; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; + +import com.google.common.base.Optional; + +import java.util.Date; + +/** + * Programmer: rhodey + */ +// TODO: this is kind of hacked together-- can to better. +public class DavAccount { + + public static final String SYNC_ACCOUNT_TYPE = "openwhispersystems.org"; + private static final String KEY_CARD_DAV_COLLECTION = "org.anhonesteffort.flock.auth.DavAccount.KEY_CARD_DAV_COLLECTION"; + + private static final String KEY_USER_ID = "KEY_USER_ID"; + private static final String KEY_AUTH_TOKEN = "KEY_AUTH_TOKEN"; + private static final String KEY_DAV_HOST_HREF = "KEY_DAV_HOST_HREF"; + + private final Account osAccount; + + private String userId; + private String authToken; + private String davHostHREF; + + public DavAccount(String userId, + String authToken, + String davHostHREF) + { + osAccount = new Account(userId, SYNC_ACCOUNT_TYPE); + + this.userId = userId; + this.authToken = authToken; + this.davHostHREF = davHostHREF; + } + + public String getUserId() { + return userId; + } + + public String getAuthToken() { + return authToken; + } + + public String getDavHostHREF() { + return davHostHREF; + } + + public Optional getCardDavCollectionPath(Context context) { + SharedPreferences preferences = context.getSharedPreferences(SYNC_ACCOUNT_TYPE, + Context.MODE_MULTI_PROCESS); + return Optional.fromNullable(preferences.getString(KEY_CARD_DAV_COLLECTION, null)); + } + + public void setCardDavCollection(Context context, String path) { + SharedPreferences preferences = context.getSharedPreferences(SYNC_ACCOUNT_TYPE, + Context.MODE_MULTI_PROCESS); + preferences.edit().putString(KEY_CARD_DAV_COLLECTION, path).commit(); + } + + public Account getOsAccount() { + return osAccount; + } + + public Bundle toBundle() { + Bundle bundle = new Bundle(); + + bundle.putString(KEY_USER_ID, userId); + bundle.putString(KEY_AUTH_TOKEN, authToken); + bundle.putString(KEY_DAV_HOST_HREF, davHostHREF); + + return bundle; + } + + public static Optional build(Bundle bundledAccount) { + if (bundledAccount == null || bundledAccount.getString(KEY_USER_ID) == null) + return Optional.absent(); + + return Optional.of(new DavAccount( + bundledAccount.getString(KEY_USER_ID), + bundledAccount.getString(KEY_AUTH_TOKEN), + bundledAccount.getString(KEY_DAV_HOST_HREF) + )); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/crypto/InvalidMacException.java b/flock/src/main/java/org/anhonesteffort/flock/crypto/InvalidMacException.java new file mode 100644 index 0000000..9c7ac3f --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/crypto/InvalidMacException.java @@ -0,0 +1,39 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ +package org.anhonesteffort.flock.crypto; + +public class InvalidMacException extends Exception { + + public InvalidMacException() { + super(); + } + + public InvalidMacException(String detailMessage) { + super(detailMessage); + } + + public InvalidMacException(Throwable throwable) { + super(throwable); + } + + public InvalidMacException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/crypto/KeyHelper.java b/flock/src/main/java/org/anhonesteffort/flock/crypto/KeyHelper.java new file mode 100644 index 0000000..6031e98 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/crypto/KeyHelper.java @@ -0,0 +1,171 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.crypto; + +import android.content.Context; +import android.util.Log; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.util.Base64; +import org.anhonesteffort.flock.util.Util; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +/** + * Programmer: rhodey + */ +public class KeyHelper { + + private static final String TAG = "org.anhonesteffort.flock.crypto.KeyHelper"; + + public static void generateAndSaveSaltAndKeyMaterial(Context context) + throws IOException, GeneralSecurityException + { + Log.d(TAG, "GENERATING SALT AND KEY MATERIAL!"); + byte[] cipherKey = KeyUtil.generateCipherKey(); + byte[] macKey = KeyUtil.generateMacKey(); + byte[] salt = KeyUtil.generateSalt(); + + Log.d(TAG, "SAVING SALT AND KEY MATERIAL!"); + KeyStore.saveCipherKey( context, cipherKey); + KeyStore.saveMacKey( context, macKey); + KeyStore.saveKeyMaterialSalt(context, salt); + + Optional encryptedKeyMaterial = buildEncryptedKeyMaterial(context); + if (encryptedKeyMaterial.isPresent()) + KeyStore.saveEncryptedKeyMaterial(context, encryptedKeyMaterial.get()); + } + + public static Optional getMasterCipher(Context context) throws IOException { + Optional cipherKeyBytes = KeyStore.getCipherKey(context); + Optional macKeyBytes = KeyStore.getMacKey(context); + + if (!cipherKeyBytes.isPresent() || !macKeyBytes.isPresent()) + return Optional.absent(); + + SecretKey cipherKey = new SecretKeySpec(cipherKeyBytes.get(), "AES"); + SecretKey macKey = new SecretKeySpec(cipherKeyBytes.get(), "SHA256"); + + return Optional.of(new MasterCipher(cipherKey, macKey)); + } + + public static Optional buildEncodedSalt(Context context) throws IOException { + Optional salt = KeyStore.getKeyMaterialSalt(context); + + if (!salt.isPresent()) + return Optional.absent(); + + return Optional.of(Base64.encodeBytes(salt.get())); + } + + public static Optional buildEncryptedKeyMaterial(Context context) + throws IOException, GeneralSecurityException + { + Optional cipherKey = KeyStore.getCipherKey(context); + Optional macKey = KeyStore.getMacKey(context); + Optional salt = KeyStore.getKeyMaterialSalt(context); + Optional masterPassphrase = KeyStore.getMasterPassphrase(context); + + if (!masterPassphrase.isPresent() || !cipherKey.isPresent() || + !macKey.isPresent() || !salt.isPresent()) + return Optional.absent(); + + SecretKey[] masterKeys = KeyUtil.getCipherAndMacKeysForPassphrase(salt.get(), masterPassphrase.get()); + SecretKey masterCipherKey = masterKeys[0]; + SecretKey masterMacKey = masterKeys[1]; + MasterCipher masterCipher = new MasterCipher(masterCipherKey, masterMacKey); + + byte[] keyMaterial = Util.combine(cipherKey.get(), macKey.get()); + byte[] encryptedKeyMaterial = masterCipher.encryptAndEncode(keyMaterial); + + return Optional.of(new String(encryptedKeyMaterial)); + } + + public static void importSaltAndEncryptedKeyMaterial(Context context, + String[] saltAndEncryptedKeyMaterial) + throws GeneralSecurityException, InvalidMacException, IOException + { + Log.d(TAG, "IMPORTING ENCRYPTED KEY MATERIAL!"); + + Optional masterPassphrase = KeyStore.getMasterPassphrase(context); + if (!masterPassphrase.isPresent()) + throw new InvalidMacException("Passphrase unavailable."); + + byte[] salt = Base64.decode(saltAndEncryptedKeyMaterial[0]); + SecretKey[] masterKeys = KeyUtil.getCipherAndMacKeysForPassphrase(salt, masterPassphrase.get()); + SecretKey masterCipherKey = masterKeys[0]; + SecretKey masterMacKey = masterKeys[1]; + MasterCipher masterCipher = new MasterCipher(masterCipherKey, masterMacKey); + byte[] plaintextKeyMaterial = masterCipher.decodeAndDecrypt(saltAndEncryptedKeyMaterial[1].getBytes()); + + boolean saltLengthValid = salt.length == KeyUtil.SALT_LENGTH_BYTES; + boolean keyMaterialLengthValid = plaintextKeyMaterial.length == (KeyUtil.CIPHER_KEY_LENGTH_BYTES + KeyUtil.MAC_KEY_LENGTH_BYTES); + + if (!saltLengthValid || !keyMaterialLengthValid) + throw new GeneralSecurityException("invalid length on salt or key material >> " + + saltLengthValid + " " + keyMaterialLengthValid); + + byte[] plaintextCipherKey = Arrays.copyOfRange(plaintextKeyMaterial, 0, KeyUtil.CIPHER_KEY_LENGTH_BYTES); + byte[] plaintextMacKey = Arrays.copyOfRange(plaintextKeyMaterial, + KeyUtil.CIPHER_KEY_LENGTH_BYTES, + plaintextCipherKey.length); + + KeyStore.saveEncryptedKeyMaterial(context, saltAndEncryptedKeyMaterial[1]); + KeyStore.saveKeyMaterialSalt( context, salt); + KeyStore.saveCipherKey( context, plaintextCipherKey); + KeyStore.saveMacKey( context, plaintextMacKey); + } + + public static boolean masterPassphraseIsValid(Context context) + throws GeneralSecurityException, IOException + { + Optional masterPassphrase = KeyStore.getMasterPassphrase(context); + if (!masterPassphrase.isPresent()) + return false; + + Optional encryptedKeyMaterial = KeyStore.getEncryptedKeyMaterial(context); + if (!encryptedKeyMaterial.isPresent()) + throw new GeneralSecurityException("Where did my key material go! XXX!!!!"); + + Optional salt = KeyStore.getKeyMaterialSalt(context); + if (!salt.isPresent()) + throw new GeneralSecurityException("Where did my salt go! XXX!!!!"); + + SecretKey[] masterKeys = KeyUtil.getCipherAndMacKeysForPassphrase(salt.get(), masterPassphrase.get()); + SecretKey masterCipherKey = masterKeys[0]; + SecretKey masterMacKey = masterKeys[1]; + MasterCipher masterCipher = new MasterCipher(masterCipherKey, masterMacKey); + + try { + + masterCipher.decodeAndDecrypt(encryptedKeyMaterial.get()); + + } catch (InvalidMacException e) { + return false; + } + + return true; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/crypto/KeyStore.java b/flock/src/main/java/org/anhonesteffort/flock/crypto/KeyStore.java new file mode 100644 index 0000000..93726ba --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/crypto/KeyStore.java @@ -0,0 +1,136 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.crypto; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.util.Base64; + +import java.io.IOException; + +/** + * Programmer: rhodey + */ +public class KeyStore { + + private static final String TAG = "org.anhonesteffort.flock.crypto.KeyStore"; + + private static final String PREFERENCES_NAME = "org.anhonesteffort.flock.crypto.KeyStore"; + private static final String KEY_MASTER_PASSPHRASE = "KEY_OLD_MASTER_PASSPHRASE"; + private static final String KEY_CIPHER_KEY = "KEY_CIPHER_KEY"; + private static final String KEY_MAC_KEY = "KEY_MAC_KEY"; + private static final String KEY_KEY_MATERIAL_SALT = "KEY_KEY_MATERIAL_SALT"; + private static final String KEY_ENCRYPTED_KEY_MATERIAL = "KEY_ENCRYPTED_KEY_MATERIAL"; + + protected static void saveCipherKey(Context context, byte[] cipherKey) { + Log.d(TAG, "SAVING CIPHER KEY MATERIAL..."); + saveBytes(context, KEY_CIPHER_KEY, cipherKey); + } + + protected static Optional getCipherKey(Context context) throws IOException { + return retrieveBytes(context, KEY_CIPHER_KEY); + } + + protected static void saveMacKey(Context context, byte[] cipherKey) { + Log.d(TAG, "SAVING MAC KEY MATERIAL..."); + saveBytes(context, KEY_MAC_KEY, cipherKey); + } + + protected static Optional getMacKey(Context context) throws IOException { + return retrieveBytes(context, KEY_MAC_KEY); + } + + protected static void saveKeyMaterialSalt(Context context, byte[] salt) { + Log.d(TAG, "SAVING SALT FOR KEY MATERIAL..."); + saveBytes(context, KEY_KEY_MATERIAL_SALT, salt); + } + + protected static Optional getKeyMaterialSalt(Context context) throws IOException { + return retrieveBytes(context, KEY_KEY_MATERIAL_SALT); + } + + public static void saveMasterPassphrase(Context context, String passphrase) { + Log.d(TAG, "SAVING MASTER PASSPHRASE..."); + saveString(context, KEY_MASTER_PASSPHRASE, passphrase); + } + + public static Optional getMasterPassphrase(Context context) { + return retrieveString(context, KEY_MASTER_PASSPHRASE); + } + + public static void saveEncryptedKeyMaterial(Context context, String encryptedKeyMaterial) { + Log.d(TAG, "SAVING ENCRYPTED KEY MATERIAL..."); + saveString(context, KEY_ENCRYPTED_KEY_MATERIAL, encryptedKeyMaterial); + } + + public static Optional getEncryptedKeyMaterial(Context context) { + return retrieveString(context, KEY_ENCRYPTED_KEY_MATERIAL); + } + + public static void invalidateKeyMaterial(Context context) { + Log.w(TAG, "INVALIDATING ALL KEY MATERIAL..."); + SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, + Context.MODE_MULTI_PROCESS); + + settings.edit().remove(KEY_CIPHER_KEY).commit(); + settings.edit().remove(KEY_MAC_KEY).commit(); + settings.edit().remove(KEY_KEY_MATERIAL_SALT).commit(); + settings.edit().remove(KEY_MASTER_PASSPHRASE).commit(); + } + + private static void saveBytes(Context context, String key, byte[] value) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, + Context.MODE_MULTI_PROCESS); + SharedPreferences.Editor editor = settings.edit(); + + editor.putString(key, Base64.encodeBytes(value)); + editor.commit(); + } + + private static void saveString(Context context, String key, String value) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, + Context.MODE_MULTI_PROCESS); + SharedPreferences.Editor editor = settings.edit(); + + editor.putString(key, value); + editor.commit(); + } + + private static Optional retrieveBytes(Context context, String key) throws IOException { + SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, + Context.MODE_MULTI_PROCESS); + String encodedValue = settings.getString(key, null); + + if (encodedValue == null) + return Optional.absent(); + + return Optional.of(Base64.decode(encodedValue)); + } + + private static Optional retrieveString(Context context, String key) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, + Context.MODE_MULTI_PROCESS); + return Optional.fromNullable(settings.getString(key, null)); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/crypto/KeyUtil.java b/flock/src/main/java/org/anhonesteffort/flock/crypto/KeyUtil.java new file mode 100644 index 0000000..a19d54a --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/crypto/KeyUtil.java @@ -0,0 +1,109 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.crypto; + +import android.util.Log; + +import org.anhonesteffort.flock.util.Base64; + +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Date; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * Programmer: rhodey + */ +public class KeyUtil { + + private static final String TAG = "org.anhonesteffort.flock.crypto.KeyUtil"; + + protected static final int CIPHER_KEY_LENGTH_BYTES = 32; + protected static final int MAC_KEY_LENGTH_BYTES = 32; + protected static final int SALT_LENGTH_BYTES = 8; + + private static final int ITERATION_COUNT_AUTH_TOKEN = 20050; + private static final int ITERATION_COUNT_KEY_MATERIAL = 20000; + + protected static byte[] generateCipherKey() throws NoSuchAlgorithmException { + Log.d(TAG, "generateCipherKey()"); + + byte[] cipherKey = new byte[CIPHER_KEY_LENGTH_BYTES]; + SecureRandom.getInstance("SHA1PRNG").nextBytes(cipherKey); + return cipherKey; + } + + protected static byte[] generateMacKey() throws NoSuchAlgorithmException { + Log.d(TAG, "generateMacKey()"); + + byte[] macKey = new byte[MAC_KEY_LENGTH_BYTES]; + SecureRandom.getInstance("SHA1PRNG").nextBytes(macKey); + + return macKey; + } + + protected static byte[] generateSalt() throws NoSuchAlgorithmException { + Log.d(TAG, "generateSalt()"); + + byte[] salt = new byte[SALT_LENGTH_BYTES]; + SecureRandom.getInstance("SHA1PRNG").nextBytes(salt); + + return salt; + } + + public static String getAuthTokenForPassphrase(String passphrase) + throws GeneralSecurityException + { + byte[] salt = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + SecretKey authKey = getKeyForPassphrase(passphrase, salt, ITERATION_COUNT_AUTH_TOKEN); + return Base64.encodeBytes(authKey.getEncoded()); + } + + protected static SecretKey[] getCipherAndMacKeysForPassphrase(byte[] salt, String passphrase) + throws GeneralSecurityException + { + SecretKey combinedKeys = getKeyForPassphrase(passphrase, salt, ITERATION_COUNT_KEY_MATERIAL); + + byte[] cipherKeyBytes = Arrays.copyOfRange(combinedKeys.getEncoded(), 0, CIPHER_KEY_LENGTH_BYTES); + byte[] macKeyBytes = Arrays.copyOfRange(combinedKeys.getEncoded(), + CIPHER_KEY_LENGTH_BYTES, + CIPHER_KEY_LENGTH_BYTES + MAC_KEY_LENGTH_BYTES); + + SecretKey cipherKey = new SecretKeySpec(cipherKeyBytes, combinedKeys.getAlgorithm()); + SecretKey macKey = new SecretKeySpec(macKeyBytes, combinedKeys.getAlgorithm()); + + return new SecretKey[] {cipherKey, macKey}; + } + + private static SecretKey getKeyForPassphrase(String passphrase, byte[] salt, int iterationCount) + throws GeneralSecurityException + { + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + PBEKeySpec keySpec = new PBEKeySpec(passphrase.toCharArray(), salt, iterationCount, (64 * 8)); + return keyFactory.generateSecret(keySpec); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/crypto/MasterCipher.java b/flock/src/main/java/org/anhonesteffort/flock/crypto/MasterCipher.java new file mode 100644 index 0000000..6808fa0 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/crypto/MasterCipher.java @@ -0,0 +1,114 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.crypto; + +import org.anhonesteffort.flock.util.Base64; +import org.anhonesteffort.flock.util.Util; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.util.Arrays; +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; + +/** + * Programmer: rhodey + */ +public class MasterCipher { + + protected static final int MAC_LENGTH_BYTES = 32; + protected static final int IV_LENGTH_BYTES = 16; + + private final SecretKey cipherKey; + private final SecretKey macKey; + + protected MasterCipher(SecretKey cipherKey, SecretKey macKey) { + this.cipherKey = cipherKey; + this.macKey = macKey; + } + + public byte[] encryptAndEncode(byte[] data) + throws IOException, GeneralSecurityException + { + Cipher encryptingCipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); + encryptingCipher.init(Cipher.ENCRYPT_MODE, cipherKey); + + Mac hmac = Mac.getInstance("HmacSHA256"); + hmac.init(macKey); + + byte[] iv = encryptingCipher.getIV(); + byte[] ciphertext = encryptingCipher.doFinal(data); + byte[] mac = hmac.doFinal(Util.combine(iv, ciphertext)); + + return Base64.encodeBytesToBytes(Util.combine(iv, ciphertext, mac)); + } + + public String encryptAndEncode(String data) + throws IOException, GeneralSecurityException + { + return new String(encryptAndEncode(data.getBytes())); + } + + public byte[] decodeAndDecrypt(byte[] encodedIvCiphertextAndMac) + throws InvalidMacException, IOException, GeneralSecurityException + { + byte[] ivCiphertextAndMac = Base64.decode(encodedIvCiphertextAndMac); + if (ivCiphertextAndMac.length <= (IV_LENGTH_BYTES + MAC_LENGTH_BYTES)) + throw new GeneralSecurityException("invalid length on decoded iv, ciphertext and mac"); + + byte[] iv = Arrays.copyOfRange(ivCiphertextAndMac, 0, IV_LENGTH_BYTES); + byte[] ciphertext = Arrays.copyOfRange(ivCiphertextAndMac, + IV_LENGTH_BYTES, + ivCiphertextAndMac.length - MAC_LENGTH_BYTES); + byte[] mac = Arrays.copyOfRange(ivCiphertextAndMac, + ivCiphertextAndMac.length - MAC_LENGTH_BYTES, + ivCiphertextAndMac.length); + + Cipher decryptingCipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + decryptingCipher.init(Cipher.DECRYPT_MODE, cipherKey, ivSpec); + + Mac hmac = Mac.getInstance("HmacSHA256"); + hmac.init(macKey); + + verifyMac(hmac, Util.combine(iv, ciphertext), mac); + + return decryptingCipher.doFinal(ciphertext); + } + + public String decodeAndDecrypt(String data) + throws InvalidMacException, IOException, GeneralSecurityException + { + return new String(decodeAndDecrypt(data.getBytes())); + } + + protected static void verifyMac(Mac hmac, byte[] theirData, byte[] theirMac) + throws InvalidMacException + { + byte[] ourMac = hmac.doFinal(theirData); + + if (!MessageDigest.isEqual(theirMac, ourMac)) + throw new InvalidMacException("INVALID MAC"); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/registration/AuthorizationException.java b/flock/src/main/java/org/anhonesteffort/flock/registration/AuthorizationException.java new file mode 100644 index 0000000..608cff3 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/registration/AuthorizationException.java @@ -0,0 +1,31 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.registration; + +/** + * Programmer: rhodey + */ +public class AuthorizationException extends RegistrationApiException { + + public AuthorizationException(String message) { + super(message); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/registration/OwsRegistration.java b/flock/src/main/java/org/anhonesteffort/flock/registration/OwsRegistration.java new file mode 100644 index 0000000..cffee72 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/registration/OwsRegistration.java @@ -0,0 +1,85 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.registration; + +import android.util.Log; + +import org.apache.http.NameValuePair; +import org.apache.http.protocol.HTTP; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.List; + +/** + * Programmer: rhodey + */ +public class OwsRegistration { + + protected static final int STATUS_OK = 200; + protected static final int STATUS_REDIRECT = 300; + protected static final int STATUS_MALFORMED_REQUEST = 400; + protected static final int STATUS_UNAUTHORIZED = 401; + protected static final int STATUS_PAYMENT_REQUIRED = 402; + protected static final int STATUS_RESOURCE_ALREADY_EXISTS = 403; + protected static final int STATUS_RESOURCE_NOT_FOUND = 404; + protected static final int STATUS_SERVER_ERROR = 500; + protected static final int STATUS_SERVICE_UNAVAILABLE = 503; + + protected static final String ACCOUNT_COLLECTION = "accounts"; + protected static final String ACCOUNT_CARD_CONTROLLER = "card"; + protected static final String PRICING_CONTROLLER = "pricing"; + + protected static final String PARAM_ACCOUNT_ID = "id"; + protected static final String PARAM_ACCOUNT_PASSWORD = "password"; + protected static final String PARAM_STRIPE_CARD_TOKEN = "stripe_card_token"; + protected static final String PARAM_AUTO_RENEW = "auto_renew"; + + protected static final String REGISTRATION_API_HOST = "flock-accounts.whispersystems.org"; + protected static final int REGISTRATION_API_PORT = 443; + protected static final String HREF_REGISTRATION_API = "https://" + REGISTRATION_API_HOST + ":" + REGISTRATION_API_PORT; + protected static final String HREF_ACCOUNT_COLLECTION = HREF_REGISTRATION_API + "/" + ACCOUNT_COLLECTION + "/"; + protected static final String HREF_PRICING = HREF_REGISTRATION_API + "/" + PRICING_CONTROLLER + "/"; + + public static final String STRIPE_PUBLIC_KEY = "pk_live_EiIuIaXaPPMgjllTlweiDYgJ"; + + protected static String getHrefForAccount(String accountId) { + return HREF_ACCOUNT_COLLECTION + accountId; + } + + protected static String getHrefForCard(String accountId) { + return HREF_ACCOUNT_COLLECTION + accountId + "/" + ACCOUNT_CARD_CONTROLLER; + } + + protected static String getHrefWithParameters(String href, List params) { + String result = href + "?"; + try { + + for (NameValuePair param : params) + result += param.getName() + "=" + URLEncoder.encode(param.getValue(), HTTP.UTF_8) + "&"; + + } catch (UnsupportedEncodingException e) { + Log.e("OwsRegistrtaion", e.toString()); + } + + return result; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/registration/RegistrationApi.java b/flock/src/main/java/org/anhonesteffort/flock/registration/RegistrationApi.java new file mode 100644 index 0000000..cc4c0ea --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/registration/RegistrationApi.java @@ -0,0 +1,381 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.registration; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.AssetManager; +import android.preference.PreferenceManager; +import android.util.Log; +import android.util.Pair; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.google.common.base.Optional; +import com.google.common.io.CharStreams; +import com.stripe.exception.CardException; + +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.registration.model.AugmentedFlockAccount; +import org.anhonesteffort.flock.registration.model.FlockCardInformation; +import org.anhonesteffort.flock.sync.OwsWebDav; +import org.anhonesteffort.flock.util.Base64; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.security.KeyStore; +import java.util.ArrayList; +import java.util.List; + +/** + * Programmer: rhodey + */ +public class RegistrationApi { + + private static final String TAG = "org.anhonesteffort.flock.registration.RegistrationApi"; + + private static final String KEY_CACHED_DAYS_REMAINING = "KEY_CACHED_DAYS_REMAINING"; + private static final String KEY_CACHED_AUTO_RENEW_ENABLED = "KEY_CACHED_AUTO_RENEW_ENABLED"; + private static final String KEY_CACHED_LAST_CHARGE_FAILED = "KEY_CACHED_LAST_CHARGE_FAILED"; + + private Context context; + private final ObjectMapper mapper; + + public RegistrationApi(Context context) { + this.context = context; + this.mapper = new ObjectMapper(); + + mapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES); + } + + private DefaultHttpClient getClient(Context context) + throws IOException, RegistrationApiClientException + { + try { + + AssetManager assetManager = context.getAssets(); + InputStream keyStoreInputStream = assetManager.open("flock.store"); + KeyStore trustStore = KeyStore.getInstance("BKS"); + + trustStore.load(keyStoreInputStream, "owsflock".toCharArray()); + + SSLSocketFactory appSSLSocketFactory = new SSLSocketFactory(trustStore); + DefaultHttpClient client = new DefaultHttpClient(); + SchemeRegistry schemeRegistry = client.getConnectionManager().getSchemeRegistry(); + Scheme httpsScheme = new Scheme("https", appSSLSocketFactory, 443); + + schemeRegistry.register(httpsScheme); + + return client; + + } catch (Exception e) { + Log.e(TAG, "caught exception while constructing HttpClient client", e); + throw new RegistrationApiClientException("caught exception while constructing HttpClient client: " + e.toString()); + } + } + + private void authorizeRequest(HttpRequestBase httpRequest, DavAccount account) { + String encodedAuth = account.getUserId() + ":" + account.getAuthToken(); + httpRequest.addHeader("Authorization", "Basic " + Base64.encodeBytes(encodedAuth.getBytes())); + } + + private void throwExceptionIfNotOK(HttpResponse response) throws RegistrationApiException { + Log.d(TAG, "response status code: " + response.getStatusLine().getStatusCode()); + + switch (response.getStatusLine().getStatusCode()) { + case OwsRegistration.STATUS_MALFORMED_REQUEST: + throw new RegistrationApiClientException("Registration API returned status malformed request."); + + case OwsRegistration.STATUS_UNAUTHORIZED: + throw new AuthorizationException("Registration API returned status unauthorized."); + + case OwsRegistration.STATUS_RESOURCE_NOT_FOUND: + throw new ResourceNotFoundException("Registration API returned status resource not found."); + + case OwsRegistration.STATUS_RESOURCE_ALREADY_EXISTS: + throw new ResourceAlreadyExistsException("Registration API returned status 403, resource already exists."); + + case OwsRegistration.STATUS_PAYMENT_REQUIRED: + throw new RegistrationApiClientException("Registration API didn't like the card token we gave it", + OwsRegistration.STATUS_PAYMENT_REQUIRED); + + case OwsRegistration.STATUS_SERVICE_UNAVAILABLE: + throw new RegistrationApiException("Registration API service is unavailable"); + + case OwsRegistration.STATUS_SERVER_ERROR: + throw new RegistrationApiException("Registration API returned status 500! 0.o"); + } + } + + private AugmentedFlockAccount buildFlockAccount(HttpResponse response) + throws RegistrationApiClientException + { + try { + + return mapper.readValue(response.getEntity().getContent(), AugmentedFlockAccount.class); + + } catch (IOException e) { + Log.e(TAG, "unable to build account from HTTP response", e); + throw new RegistrationApiClientException("unable to build account from HTTP response."); + } + } + + private FlockCardInformation buildFlockCardInformation(HttpResponse response) + throws RegistrationApiClientException + { + try { + + return mapper.readValue(response.getEntity().getContent(), FlockCardInformation.class); + + } catch (IOException e) { + Log.e(TAG, "unable to build card information from HTTP response", e); + throw new RegistrationApiClientException("unable to build card information from HTTP response."); + } + } + + public Double getCostPerYearUsd() + throws IOException, RegistrationApiException + { + HttpGet httpGet = new HttpGet(OwsRegistration.HREF_PRICING); + DefaultHttpClient httpClient = getClient(context); + HttpResponse response = httpClient.execute(httpGet); + InputStreamReader reader = new InputStreamReader(response.getEntity().getContent()); + + throwExceptionIfNotOK(response); + + return Double.valueOf(CharStreams.toString(reader)); + } + + public boolean isAuthenticated(DavAccount account) + throws IOException, RegistrationApiException + { + try { + + HttpGet httpGet = new HttpGet(OwsRegistration.getHrefForAccount(account.getUserId())); + DefaultHttpClient httpClient = getClient(context); + authorizeRequest(httpGet, account); + + HttpResponse response = httpClient.execute(httpGet); + throwExceptionIfNotOK(response); + + return true; + + } catch (AuthorizationException e) { + return false; + } + } + + public AugmentedFlockAccount createAccount(DavAccount account) + throws RegistrationApiException, IOException + { + Log.d(TAG, "createAccount()"); + + List nameValuePairs = new ArrayList(1); + nameValuePairs.add(new BasicNameValuePair(OwsRegistration.PARAM_ACCOUNT_ID, account.getUserId())); + nameValuePairs.add(new BasicNameValuePair(OwsRegistration.PARAM_ACCOUNT_PASSWORD, account.getAuthToken())); + + String HREF = OwsRegistration.getHrefWithParameters(OwsRegistration.HREF_ACCOUNT_COLLECTION, + nameValuePairs); + HttpPost httpPost = new HttpPost(HREF); + DefaultHttpClient httpClient = getClient(context); + + HttpResponse response = httpClient.execute(httpPost); + throwExceptionIfNotOK(response); + + AugmentedFlockAccount flockAccount = buildFlockAccount(response); + + cacheSubscriptionDetails(context, + flockAccount.getDaysRemaining(), + flockAccount.getAutoRenewEnabled(), + flockAccount.getLastStripeChargeFailed()); + return flockAccount; + } + + public AugmentedFlockAccount getAccount(DavAccount account) + throws RegistrationApiException, IOException + { + String HREF = OwsRegistration.getHrefForAccount(account.getUserId()); + HttpGet httpGet = new HttpGet(HREF); + DefaultHttpClient httpClient = getClient(context); + authorizeRequest(httpGet, account); + + HttpResponse response = httpClient.execute(httpGet); + throwExceptionIfNotOK(response); + + AugmentedFlockAccount flockAccount = buildFlockAccount(response); + + cacheSubscriptionDetails(context, + flockAccount.getDaysRemaining(), + flockAccount.getAutoRenewEnabled(), + flockAccount.getLastStripeChargeFailed()); + return flockAccount; + } + + private static void cacheSubscriptionDetails(Context context, + Long daysRemaining, + Boolean autoRenewEnabled, + Boolean lastChargeFailed) + { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + preferences.edit() + .putLong(KEY_CACHED_DAYS_REMAINING, daysRemaining) + .putBoolean(KEY_CACHED_AUTO_RENEW_ENABLED, autoRenewEnabled) + .putBoolean(KEY_CACHED_LAST_CHARGE_FAILED, lastChargeFailed) + .commit(); + } + + public static Optional> getCachedSubscriptionDetails(Context context) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + if (preferences.getLong(KEY_CACHED_DAYS_REMAINING, -1) == -1) + return Optional.absent(); + + return Optional.of(new Pair( + preferences.getLong(KEY_CACHED_DAYS_REMAINING, 0), + new Boolean[] { + preferences.getBoolean(KEY_CACHED_AUTO_RENEW_ENABLED, false), + preferences.getBoolean(KEY_CACHED_LAST_CHARGE_FAILED, true) + } + )); + } + + public Optional getCard(DavAccount account) + throws RegistrationApiException, IOException + { + String HREF = OwsRegistration.getHrefForCard(account.getUserId()); + HttpGet httpGet = new HttpGet(HREF); + DefaultHttpClient httpClient = getClient(context); + authorizeRequest(httpGet, account); + + HttpResponse response = httpClient.execute(httpGet); + + try { + + throwExceptionIfNotOK(response); + + } catch (ResourceNotFoundException e) { + return Optional.absent(); + } + + return Optional.of(buildFlockCardInformation(response)); + } + + public void setAccountPassword(DavAccount account, String newPassword) + throws RegistrationApiException, IOException + { + Log.d(TAG, "setAccountPassword()"); + + List nameValuePairs = new ArrayList(1); + nameValuePairs.add(new BasicNameValuePair(OwsRegistration.PARAM_ACCOUNT_PASSWORD, newPassword)); + + String HREF = OwsRegistration.getHrefWithParameters(OwsRegistration.getHrefForAccount(account.getUserId()), nameValuePairs); + HttpPut httpPut = new HttpPut(HREF); + DefaultHttpClient httpClient = getClient(context); + authorizeRequest(httpPut, account); + + HttpResponse response = httpClient.execute(httpPut); + throwExceptionIfNotOK(response); + } + + public void updateAccountStripeCard(DavAccount account, String stripeCardToken) + throws CardException, RegistrationApiException, IOException + { + List nameValuePairs = new ArrayList(1); + nameValuePairs.add(new BasicNameValuePair(OwsRegistration.PARAM_STRIPE_CARD_TOKEN, stripeCardToken)); + + String HREF = OwsRegistration.getHrefWithParameters(OwsRegistration.getHrefForAccount(account.getUserId()), nameValuePairs); + HttpPut httpPut = new HttpPut(HREF); + DefaultHttpClient httpClient = getClient(context); + authorizeRequest(httpPut, account); + + HttpResponse response = httpClient.execute(httpPut); + + try { + + throwExceptionIfNotOK(response); + + } catch (RegistrationApiClientException e) { + if (e.getStatus() == OwsRegistration.STATUS_PAYMENT_REQUIRED) + throw new CardException("server rejected card", "hack", "hack", null); + else + throw e; + } + + Optional> subscriptionDetails = getCachedSubscriptionDetails(context); + if (!subscriptionDetails.isPresent()) + return; + + cacheSubscriptionDetails(context, + subscriptionDetails.get().first, + subscriptionDetails.get().second[0], + false); + } + + public void setAccountAutoRenew(DavAccount account, Boolean autoRenewEnabled) + throws RegistrationApiException, IOException + { + List nameValuePairs = new ArrayList(1); + nameValuePairs.add(new BasicNameValuePair(OwsRegistration.PARAM_AUTO_RENEW, autoRenewEnabled.toString())); + + String HREF = OwsRegistration.getHrefWithParameters(OwsRegistration.getHrefForAccount(account.getUserId()), nameValuePairs); + HttpPut httpPut = new HttpPut(HREF); + DefaultHttpClient httpClient = getClient(context); + authorizeRequest(httpPut, account); + + HttpResponse response = httpClient.execute(httpPut); + throwExceptionIfNotOK(response); + + Optional> subscriptionDetails = getCachedSubscriptionDetails(context); + if (!subscriptionDetails.isPresent()) + return; + + cacheSubscriptionDetails(context, + subscriptionDetails.get().first, + autoRenewEnabled, + subscriptionDetails.get().second[1]); + } + + public void deleteAccount(DavAccount account) + throws RegistrationApiException, IOException + { + Log.d(TAG, "deleteAccount()"); + + HttpDelete httpDelete = new HttpDelete(OwsRegistration.getHrefForAccount(account.getUserId())); + DefaultHttpClient httpClient = getClient(context); + authorizeRequest(httpDelete, account); + + HttpResponse response = httpClient.execute(httpDelete); + throwExceptionIfNotOK(response); + + cacheSubscriptionDetails(context, 0L, false, true); + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/registration/RegistrationApiClientException.java b/flock/src/main/java/org/anhonesteffort/flock/registration/RegistrationApiClientException.java new file mode 100644 index 0000000..82a5686 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/registration/RegistrationApiClientException.java @@ -0,0 +1,35 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.registration; + +/** + * Programmer: rhodey + */ +public class RegistrationApiClientException extends RegistrationApiException { + + public RegistrationApiClientException(String message) { + super(message); + } + + public RegistrationApiClientException(String message, int status) { + super(message, status); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/registration/RegistrationApiException.java b/flock/src/main/java/org/anhonesteffort/flock/registration/RegistrationApiException.java new file mode 100644 index 0000000..b2dc5f5 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/registration/RegistrationApiException.java @@ -0,0 +1,42 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.registration; + +/** + * Programmer: rhodey + */ +public class RegistrationApiException extends Exception { + + private int status = -1; + + public RegistrationApiException(String message) { + super(message); + } + + public RegistrationApiException(String message, int status) { + super(message); + this.status = status; + } + + public int getStatus() { + return status; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/registration/ResourceAlreadyExistsException.java b/flock/src/main/java/org/anhonesteffort/flock/registration/ResourceAlreadyExistsException.java new file mode 100644 index 0000000..6507fbe --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/registration/ResourceAlreadyExistsException.java @@ -0,0 +1,31 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.registration; + +/** + * Programmer: rhodey + */ +public class ResourceAlreadyExistsException extends RegistrationApiException { + + public ResourceAlreadyExistsException(String message) { + super(message); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/registration/ResourceNotFoundException.java b/flock/src/main/java/org/anhonesteffort/flock/registration/ResourceNotFoundException.java new file mode 100644 index 0000000..ccd8d44 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/registration/ResourceNotFoundException.java @@ -0,0 +1,31 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.registration; + +/** + * Programmer: rhodey + */ +public class ResourceNotFoundException extends RegistrationApiException { + + public ResourceNotFoundException(String message) { + super(message); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/registration/model/AugmentedFlockAccount.java b/flock/src/main/java/org/anhonesteffort/flock/registration/model/AugmentedFlockAccount.java new file mode 100644 index 0000000..5eebf13 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/registration/model/AugmentedFlockAccount.java @@ -0,0 +1,78 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.registration.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Date; +import java.util.List; + +/** + * Programmer: rhodey + */ +public class AugmentedFlockAccount extends FlockAccount { + + @JsonProperty + protected List subscriptions; + + public AugmentedFlockAccount(FlockAccount account, List subscriptions) { + super(account.getId(), account.getSalt(), account.getPasswordSha512(), + account.getStripeCustomerId(), account.getCreateDate(), account.getLastStripeChargeFailed(), + account.getAutoRenewEnabled()); + + this.subscriptions = subscriptions; + } + + public AugmentedFlockAccount() {} + + public List getSubscriptions() { + return subscriptions; + } + + @JsonIgnore + public Long getDaysRemaining() { + long days_expired = 0; + + for (int i = 0; i < subscriptions.size(); i++) { + if (i == 0) + days_expired = ((new Date().getTime() - subscriptions.get(i).getCreateDate().getTime()) + / (1000 * 60 * 60 * 24)); + days_expired -= subscriptions.get(i).getDaysCredit(); + } + + return -1 * days_expired; + } + + @Override + public boolean equals(Object other) { + if (other == null) return false; + if (!(other instanceof AugmentedFlockAccount)) return false; + + AugmentedFlockAccount that = (AugmentedFlockAccount)other; + return super.equals(that) && this.subscriptions.equals(that.subscriptions); + } + + @Override + public int hashCode() { + return super.hashCode() ^ subscriptions.hashCode(); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/registration/model/FlockAccount.java b/flock/src/main/java/org/anhonesteffort/flock/registration/model/FlockAccount.java new file mode 100644 index 0000000..9329537 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/registration/model/FlockAccount.java @@ -0,0 +1,184 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.registration.model; + +import android.os.Bundle; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.DateDeserializers; +import com.fasterxml.jackson.databind.ser.std.DateSerializer; +import com.google.common.base.Optional; +import org.anhonesteffort.flock.util.Base64; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Date; + +/** + * Programmer: rhodey + * Date: 1/2/14 + */ +public class FlockAccount { + + private static final String KEY_ACCOUNT_ID = "KEY_ACCOUNT_ID"; + private static final String KEY_SALT = "KEY_ACCOUNT_SALT"; + private static final String KEY_PASSWORD_SHA512 = "KEY_ACCOUNT_PASSWORD_SHA512"; + private static final String KEY_STRIPE_CUSTOMER_ID = "KEY_ACCOUNT_STRIPE_CUSTOMER_ID"; + private static final String KEY_CREATE_DATE = "KEY_ACCOUNT_CREATE_DATE"; + private static final String KEY_LAST_STRIPE_CHARGE_FAILED = "KEY_ACCOUNT_LAST_STRIPE_CHARGE_FAILED"; + private static final String KEY_AUTO_RENEW_ENABLED = "KEY_ACCOUNT_AUTO_RENEW_ENABLED"; + + @JsonProperty + protected String id; + + @JsonProperty + protected String salt; + + @JsonProperty + protected String passwordSha512; + + @JsonProperty + protected String stripeCustomerId; + + @JsonProperty + @JsonSerialize(using = DateSerializer.class) + @JsonDeserialize(using = DateDeserializers.DateDeserializer.class) + protected Date createDate; + + @JsonProperty + protected Boolean lastStripeChargeFailed; + + @JsonProperty + protected Boolean autoRenewEnabled; + + public FlockAccount(String id, + String passwordSha512, + String salt, + String stripeCustomerId, + Date createDate, + Boolean lastStripeChargeFailed, + Boolean autoRenewEnabled) + { + this.id = id; + this.salt = salt; + this.passwordSha512 = passwordSha512; + this.stripeCustomerId = stripeCustomerId; + this.createDate = createDate; + this.lastStripeChargeFailed = lastStripeChargeFailed; + this.autoRenewEnabled = autoRenewEnabled; + } + + public FlockAccount() {} + + private String getHashedPassword(String password) { + try { + + MessageDigest digest = MessageDigest.getInstance("SHA-512"); + digest.update(password.getBytes()); + return Base64.encodeBytes(digest.digest()); + + } catch (NoSuchAlgorithmException e) { + return null; + } + } + + public String getId() { + return id.toLowerCase(); + } + + public String getSalt() { + return salt; + } + + public String getPasswordSha512() { + return passwordSha512; + } + + public void setPassword(String password) { + this.passwordSha512 = getHashedPassword(password); + } + + public String getStripeCustomerId() { + return stripeCustomerId; + } + + public Date getCreateDate() { + return createDate; + } + + public Boolean getLastStripeChargeFailed() { + return lastStripeChargeFailed; + } + + public Boolean getAutoRenewEnabled() { + return autoRenewEnabled; + } + + public Bundle toBundle() { + Bundle bundle = new Bundle(); + + bundle.putString(KEY_ACCOUNT_ID, id); + bundle.putString(KEY_STRIPE_CUSTOMER_ID, stripeCustomerId); + bundle.putString(KEY_PASSWORD_SHA512, passwordSha512); + bundle.putLong(KEY_CREATE_DATE, createDate.getTime()); + bundle.putBoolean(KEY_LAST_STRIPE_CHARGE_FAILED, lastStripeChargeFailed); + bundle.putBoolean(KEY_AUTO_RENEW_ENABLED, autoRenewEnabled); + + return bundle; + } + + public static Optional build(Bundle bundledAccount) { + if (bundledAccount == null || bundledAccount.getString(KEY_ACCOUNT_ID) == null) + return Optional.absent(); + + return Optional.of(new FlockAccount(bundledAccount.getString(KEY_ACCOUNT_ID), + bundledAccount.getString(KEY_SALT), + bundledAccount.getString(KEY_PASSWORD_SHA512), + bundledAccount.getString(KEY_STRIPE_CUSTOMER_ID), + new Date(bundledAccount.getLong(KEY_CREATE_DATE)), + bundledAccount.getBoolean(KEY_LAST_STRIPE_CHARGE_FAILED), + bundledAccount.getBoolean(KEY_AUTO_RENEW_ENABLED))); + } + + @Override + public boolean equals(Object other) { + if (other == null) return false; + if (!(other instanceof FlockAccount)) return false; + + FlockAccount that = (FlockAccount)other; + return this.id.equals(that.id) && + this.salt.equals(that.salt) && + this.passwordSha512.equals(that.passwordSha512) && + this.stripeCustomerId.equals(that.stripeCustomerId) && + this.createDate.equals(that.createDate) && + this.lastStripeChargeFailed.equals(that.lastStripeChargeFailed) && + this.autoRenewEnabled.equals(that.autoRenewEnabled); + } + + @Override + public int hashCode() { + return id.hashCode() ^ salt.hashCode() ^ passwordSha512.hashCode() ^ + stripeCustomerId.hashCode() ^ createDate.hashCode() ^ + lastStripeChargeFailed.hashCode() ^ autoRenewEnabled.hashCode(); + } + +} \ No newline at end of file diff --git a/flock/src/main/java/org/anhonesteffort/flock/registration/model/FlockCardInformation.java b/flock/src/main/java/org/anhonesteffort/flock/registration/model/FlockCardInformation.java new file mode 100644 index 0000000..8fecb4f --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/registration/model/FlockCardInformation.java @@ -0,0 +1,107 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.registration.model; + +import android.os.Bundle; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Optional; + +/** + * Programmer: rhodey + */ +public class FlockCardInformation { + + private static final String KEY_ACCOUNT_ID = "KEY_CARD_ACCOUNT_ID"; + private static final String KEY_LAST_FOUR = "KEY_CARD_LAST_FOUR"; + private static final String KEY_EXPIRATION = "KEY_CARD_EXPIRATION"; + + @JsonProperty + protected String accountId; + + @JsonProperty + protected String cardLastFour; + + @JsonProperty + protected String cardExpiration; + + public FlockCardInformation(String accountId, String cardLastFour, String cardExpiration) { + this.accountId = accountId; + this.cardLastFour = cardLastFour; + this.cardExpiration = cardExpiration; + } + + public FlockCardInformation() {} + + public String getAccountId() { + return accountId; + } + + public String getCardLastFour() { + return cardLastFour; + } + + public String getCardExpiration() { + if (cardExpiration.length() == 7) + return cardExpiration.substring(0, 3) + cardExpiration.substring(5); + + return cardExpiration; + } + + public Bundle toBundle() { + Bundle bundle = new Bundle(); + + bundle.putString(KEY_ACCOUNT_ID, accountId); + bundle.putString(KEY_LAST_FOUR, cardLastFour); + bundle.putString(KEY_EXPIRATION, cardExpiration); + + return bundle; + } + + public static Optional build(Bundle bundledCardInformation) { + if (bundledCardInformation == null || + bundledCardInformation.getString(KEY_ACCOUNT_ID) == null) + return Optional.absent(); + + return Optional.of(new FlockCardInformation( + bundledCardInformation.getString(KEY_ACCOUNT_ID), + bundledCardInformation.getString(KEY_LAST_FOUR), + bundledCardInformation.getString(KEY_EXPIRATION)) + ); + } + + @Override + public boolean equals(Object other) { + if (other == null) return false; + if (!(other instanceof FlockCardInformation)) return false; + + FlockCardInformation that = (FlockCardInformation)other; + return this.accountId.equals(that.accountId) && + this.cardLastFour.equals(that.cardLastFour) && + this.cardExpiration.equals(that.cardExpiration); + } + + @Override + public int hashCode() { + return accountId.hashCode() ^ cardLastFour.hashCode() ^ cardExpiration.hashCode(); + } + +} + diff --git a/flock/src/main/java/org/anhonesteffort/flock/registration/model/FlockSubscription.java b/flock/src/main/java/org/anhonesteffort/flock/registration/model/FlockSubscription.java new file mode 100644 index 0000000..c21ac87 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/registration/model/FlockSubscription.java @@ -0,0 +1,108 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.registration.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.DateDeserializers; +import com.fasterxml.jackson.databind.ser.std.DateSerializer; + +import java.util.Date; + +/** + * Programmer: rhodey + */ +public class FlockSubscription { + + @JsonProperty + protected String accountId; + + @JsonProperty + protected String paymentId; + + @JsonProperty + @JsonSerialize(using = DateSerializer.class) + @JsonDeserialize(using = DateDeserializers.DateDeserializer.class) + protected Date createDate; + + @JsonProperty + protected Integer daysCredit; + + @JsonProperty + protected Double costUsd; + + public FlockSubscription() {} + + public FlockSubscription(String accountId, + String paymentId, + Date createDate, + Integer days_credit, + Double costUsd) + { + this.accountId = accountId; + this.paymentId = paymentId; + this.createDate = createDate; + this.daysCredit = days_credit; + this.costUsd = costUsd; + } + + public String getAccountId() { + return accountId; + } + + public String getPaymentId() { + return paymentId; + } + + public Date getCreateDate() { + return createDate; + } + + public int getDaysCredit() { + return daysCredit; + } + + public void setDaysCredit(int days_credit) { + daysCredit = days_credit; + } + + public Double getCostUsd() { + return costUsd; + } + + @Override + public boolean equals(Object other) { + if (other == null) return false; + if (!(other instanceof FlockSubscription)) return false; + + FlockSubscription that = (FlockSubscription)other; + return this.accountId.equals(that.accountId) && this.paymentId.equals(that.paymentId) && + this.createDate.equals(that.createDate) && this.daysCredit.equals(that.daysCredit) && + this.costUsd.equals(that.costUsd); + } + + @Override + public int hashCode() { + return accountId.hashCode() ^ paymentId.hashCode() ^ createDate.hashCode() ^ + daysCredit.hashCode() ^ costUsd.hashCode(); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/AbstractDavSyncAdapter.java b/flock/src/main/java/org/anhonesteffort/flock/sync/AbstractDavSyncAdapter.java new file mode 100644 index 0000000..a02e25b --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/AbstractDavSyncAdapter.java @@ -0,0 +1,238 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync; + +import android.accounts.Account; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.AbstractThreadedSyncAdapter; +import android.content.Context; +import android.content.Intent; +import android.content.OperationApplicationException; +import android.content.SharedPreferences; +import android.content.SyncResult; +import android.os.RemoteException; +import android.support.v4.app.NotificationCompat; +import android.util.Log; + +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.CorrectPasswordActivity; +import org.anhonesteffort.flock.DavAccountHelper; +import org.anhonesteffort.flock.ManageSubscriptionActivity; +import org.anhonesteffort.flock.R; +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.crypto.InvalidMacException; +import org.anhonesteffort.flock.sync.addressbook.AddressbookSyncScheduler; +import org.anhonesteffort.flock.sync.calendar.CalendarsSyncScheduler; +import org.anhonesteffort.flock.sync.key.KeySyncScheduler; +import org.anhonesteffort.flock.webdav.InvalidComponentException; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import javax.net.ssl.SSLException; + +/** + * Programmer: rhodey + * Date: 3/9/14 + */ +public abstract class AbstractDavSyncAdapter extends AbstractThreadedSyncAdapter { + + private static final String TAG = "org.anhonesteffort.flock.sync.AbstractDavSyncAdapter"; + + private static final int ID_NOTIFICATION_AUTH = 1020; + private static final int ID_NOTIFICATION_SUBSCRIPTION = 1021; + + private static final String PREFERENCES_NAME = "AbstractDavSyncAdapter.PREFERENCES_NAME"; + private static final String KEY_VOID_AUTH_NOTIFICATIONS = "KEY_VOID_AUTH_NOTIFICATIONS"; + + public AbstractDavSyncAdapter(Context context) { + super(context, true); + } + + protected abstract String getAuthority(); + + public static void handleException(Context context, Exception e, SyncResult result) { + if (e instanceof DavException) { + DavException ex = (DavException) e; + Log.e(TAG, "error code: " + ex.getErrorCode() + ", status phrase: " + ex.getStatusPhrase(), e); + + if (ex.getErrorCode() == DavServletResponse.SC_UNAUTHORIZED) + result.stats.numAuthExceptions++; + else if (ex.getErrorCode() == OwsWebDav.STATUS_PAYMENT_REQUIRED) + result.stats.numSkippedEntries++; + else if (ex.getErrorCode() != DavServletResponse.SC_PRECONDITION_FAILED) + result.stats.numParseExceptions++; + } + + else if (e instanceof InvalidComponentException) { + InvalidComponentException ex = (InvalidComponentException) e; + result.stats.numParseExceptions++; + Log.e(TAG, ex.toString(), ex); + } + + // server is giving us funky stuff... + else if (e instanceof PropertyParseException) { + PropertyParseException ex = (PropertyParseException) e; + result.stats.numParseExceptions++; + Log.e(TAG, ex.toString(), ex); + } + + // client is doing funky stuff... + else if (e instanceof RemoteException || e instanceof OperationApplicationException) + result.stats.numParseExceptions++; + + /* + NOTICE: MAC errors are only expected upon initial import of encrypted key material. + A MAC error here means there is remote content on a remote collection which has the + encrypted key material property and valid encryption prefix but invalid ciphertext. + TODO: should probably delete this property or component? + */ + else if (e instanceof InvalidMacException) { + Log.e(TAG, "BAD MAC IN SYNC!!! 0.o ", e); + result.stats.numParseExceptions++; + } + else if (e instanceof GeneralSecurityException) { + Log.e(TAG, "crypto problems in sync 0.u ", e); + result.stats.numParseExceptions++; + } + + else if (e instanceof SSLException) { + Log.e(TAG, "SSL PROBLEM IN SYNC!!! 0.o ", e); + result.stats.numIoExceptions++; + } + else if (e instanceof IOException) { + Log.e(TAG, "who knows...", e); + result.stats.numIoExceptions++; + } + + else { + result.stats.numParseExceptions++; + Log.e(TAG, "DID NOT CATCH THIS EXCEPTION CORRECTLY!!! >> " + e.toString()); + } + } + + public static void disableAuthNotificationsForRunningAdapters(Context context, Account account) { + AddressbookSyncScheduler addressbookSync = new AddressbookSyncScheduler(context); + CalendarsSyncScheduler calendarSync = new CalendarsSyncScheduler(context); + KeySyncScheduler keySync = new KeySyncScheduler(context); + SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_MULTI_PROCESS); + + if (addressbookSync.syncInProgress(account)) { + settings.edit().putBoolean(KEY_VOID_AUTH_NOTIFICATIONS + addressbookSync.getAuthority(), true).commit(); + Log.e(TAG, "disabling auth notifications for " + addressbookSync.getAuthority()); + } + + if (calendarSync.syncInProgress(account)) { + settings.edit().putBoolean(KEY_VOID_AUTH_NOTIFICATIONS + calendarSync.getAuthority(), true).commit(); + Log.e(TAG, "disabling auth notifications for " + calendarSync.getAuthority()); + } + + if (keySync.syncInProgress(account)) { + settings.edit().putBoolean(KEY_VOID_AUTH_NOTIFICATIONS + keySync.getAuthority(), true).commit(); + Log.e(TAG, "disabling auth notifications for " + keySync.getAuthority()); + } + } + + private boolean isAuthNotificationDisabled() { + SharedPreferences settings = getContext().getSharedPreferences(PREFERENCES_NAME, Context.MODE_MULTI_PROCESS); + + if (settings.getBoolean(KEY_VOID_AUTH_NOTIFICATIONS + getAuthority(), false)) { + settings.edit().putBoolean(KEY_VOID_AUTH_NOTIFICATIONS + getAuthority(), false).commit(); + Log.e(TAG, "auth notification is disabled for " + getAuthority()); + return true; + } + + Log.e(TAG, "auth notification is not disabled for " + getAuthority()); + return false; + } + + public static void showAuthNotificationAndInvalidatePassword(Context context) { + Log.d(TAG, "showAuthNotificationAndInvalidatePassword()"); + + DavAccountHelper.invalidateAccountPassword(context); + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context); + + notificationBuilder.setContentTitle(context.getString(R.string.notification_flock_login_error)); + notificationBuilder.setContentText(context.getString(R.string.notification_tap_to_correct_password)); + notificationBuilder.setSmallIcon(R.drawable.alert_warning_light); + notificationBuilder.setAutoCancel(true); + + Intent clickIntent = new Intent(context, CorrectPasswordActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity(context, + 0, + clickIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + notificationBuilder.setContentIntent(pendingIntent); + + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(ID_NOTIFICATION_AUTH, notificationBuilder.build()); + } + + public static void cancelAuthNotification(Context context) { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(ID_NOTIFICATION_AUTH); + } + + public static void showSubscriptionExpiredNotification(Context context) { + Log.d(TAG, "showSubscriptionExpiredNotification()"); + + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context); + + notificationBuilder.setContentTitle(context.getString(R.string.notification_flock_subscription_expired)); + notificationBuilder.setContentText(context.getString(R.string.notification_tap_to_update_subscription)); + notificationBuilder.setSmallIcon(R.drawable.alert_warning_light); + notificationBuilder.setAutoCancel(true); + + Intent clickIntent = new Intent(context, ManageSubscriptionActivity.class); + Optional account = DavAccountHelper.getAccount(context); + + clickIntent.putExtra(ManageSubscriptionActivity.KEY_DAV_ACCOUNT_BUNDLE, account.get().toBundle()); + + PendingIntent pendingIntent = PendingIntent.getActivity(context, + 0, + clickIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + notificationBuilder.setContentIntent(pendingIntent); + + NotificationManager notificationManager = + (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(ID_NOTIFICATION_SUBSCRIPTION, notificationBuilder.build()); + } + + public static void cancelSubscriptionExpiredNotification(Context context) { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(ID_NOTIFICATION_SUBSCRIPTION); + } + + protected void showNotifications(SyncResult result) { + if (result.stats.numAuthExceptions > 0 && !isAuthNotificationDisabled()) + showAuthNotificationAndInvalidatePassword(getContext()); + if (result.stats.numSkippedEntries > 0) + showSubscriptionExpiredNotification(getContext()); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/AbstractDavSyncWorker.java b/flock/src/main/java/org/anhonesteffort/flock/sync/AbstractDavSyncWorker.java new file mode 100644 index 0000000..0945892 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/AbstractDavSyncWorker.java @@ -0,0 +1,623 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync; + +import android.content.Context; +import android.content.OperationApplicationException; +import android.content.SyncResult; +import android.os.RemoteException; +import android.util.Log; +import android.util.Pair; + +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.crypto.InvalidMacException; +import org.anhonesteffort.flock.webdav.ComponentETagPair; +import org.anhonesteffort.flock.webdav.InvalidComponentException; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.anhonesteffort.flock.webdav.caldav.CalDavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.xml.Namespace; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +/** + * Programmer: rhodey + */ +public abstract class AbstractDavSyncWorker { + + private static final String TAG = "org.anhonesteffort.flock.sync.AbstractDavSyncWorker"; + + protected Context context; + protected AbstractLocalComponentCollection localCollection; + protected HidingDavCollection remoteCollection; + + protected Optional localCTag = Optional.absent(); + protected Optional remoteCTag = Optional.absent(); + + public AbstractDavSyncWorker(Context context, + AbstractLocalComponentCollection localCollection, + HidingDavCollection remoteCollection) + { + this.context = context; + this.localCollection = localCollection; + this.remoteCollection = remoteCollection; + } + + public void run(SyncResult result, boolean force_sync) { + Log.d(TAG, "now syncing local: " + localCollection.getPath() + + " with remote: " + remoteCollection.getPath()); + + try { + + localCTag = localCollection.getCTag(); + remoteCTag = remoteCollection.getCTag(); + + if (localCTag.isPresent()) + Log.d(TAG, "local ctag pre push local: " + localCTag.get()); + else + Log.d(TAG, "local ctag not present pre push local"); + + if (remoteCTag.isPresent()) + Log.d(TAG, "remote ctag pre push local: " + remoteCTag.get()); + else + Log.d(TAG, "remote ctag not present pre push local"); + + pushLocallyCreatedProperties(result); + pushLocallyChangedProperties(result); + + pushLocallyDeletedComponents(result); + pushLocallyChangedComponents(result); + pushLocallyCreatedComponents(result); + + boolean pull_remote = force_sync || result.stats.numInserts > 0 || + result.stats.numUpdates > 0 || result.stats.numDeletes > 0; + + if (!pull_remote) { + if (!localCTag.isPresent()) + pull_remote = true; + else + pull_remote = remoteCTag.isPresent() && !localCTag.get().equals(remoteCTag.get()); + } + + if (pull_remote) { + remoteCTag = remoteCollection.getCTag(); + + pullRemotelyCreatedProperties(result); + pullRemotelyChangedProperties(result); + + pullRemotelyCreatedComponents(result); + pullRemotelyChangedComponents(result); + purgeRemotelyDeletedComponents(result); + + if (remoteCTag.isPresent()) { + Log.d(TAG, "remote ctag post pull remote: " + remoteCTag.get()); + + if (result.stats.numAuthExceptions > 0 || + result.stats.numSkippedEntries > 0 || + result.stats.numParseExceptions > 0 || + result.stats.numIoExceptions > 0) + { + Log.w(TAG, "sync result has errors, will not save remote CTag to local collection"); + return; + } + + localCollection.setCTag(remoteCTag.get()); + localCollection.commitPendingOperations(); + + if (localCollection.getCTag().isPresent()) + Log.d(TAG, "local ctag post pull remote: " + localCollection.getCTag().get()); + } + else + throw new PropertyParseException("Remote collection is missing CTag, things could get funny.", + remoteCollection.getPath(), CalDavConstants.PROPERTY_NAME_CTAG); + } + + } catch (RemoteException e) { + result.stats.numParseExceptions++; + Log.e(TAG, "caught RemoteException while updating ctags! " + e.toString()); + } catch (OperationApplicationException e) { + result.stats.numParseExceptions++; + Log.e(TAG, "caught OperationApplicationException while updating ctags! " + e.toString()); + } catch (PropertyParseException e) { + result.stats.numParseExceptions++; + Log.e(TAG, "caught PropertyParseException while updating ctags! " + e.toString()); + } + } + + protected abstract Namespace getNamespace(); + + protected abstract boolean componentHasUid(T component); + + protected void pushLocallyCreatedProperties(SyncResult result) { + Log.d(TAG, "pushLocallyCreatedProperties()"); + + try { + + Optional localDisplayName = localCollection.getDisplayName(); + if (localDisplayName.isPresent()) { + + Optional remoteDisplayName = remoteCollection.getHiddenDisplayName(); + if (!remoteDisplayName.isPresent()) { + Log.d(TAG, "remote display name not present, setting using local"); + remoteCollection.setHiddenDisplayName(localDisplayName.get()); + result.stats.numInserts++; + } + } + + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (PropertyParseException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (DavException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (InvalidMacException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (GeneralSecurityException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } + + protected void pushLocallyChangedProperties(SyncResult result) { + Log.d(TAG, "pushLocallyChangedProperties()"); + + try { + + if (localCTag.isPresent() && remoteCTag.isPresent() && localCTag.get().equals(remoteCTag.get())) { + Optional localDisplayName = localCollection.getDisplayName(); + if (localDisplayName.isPresent()) { + + Optional remoteDisplayName = remoteCollection.getHiddenDisplayName(); + if (remoteDisplayName.isPresent() && !localDisplayName.get().equals(remoteDisplayName.get())) { + Log.d(TAG, "remote display name present, updating using local"); + remoteCollection.setHiddenDisplayName(localDisplayName.get()); + result.stats.numUpdates++; + } + } + } + + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (PropertyParseException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (DavException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (InvalidMacException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (GeneralSecurityException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } + + protected void pushLocallyDeletedComponents(SyncResult result) { + Log.d(TAG, "pushLocallyDeletedComponents()"); + + try { + + List> deletedIds = localCollection.getDeletedComponentIds(); + Log.d(TAG, "found + " + deletedIds.size() + " locally deleted components"); + + for (Pair componentId : deletedIds) { + Log.d(TAG, "removing remote component: (" + componentId.first + ", " + componentId.second + ")"); + + try { + + remoteCollection.removeComponent(componentId.second); + localCollection.removeComponent(componentId.first); + localCollection.commitPendingOperations(); + result.stats.numDeletes++; + + } catch (DavException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (OperationApplicationException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } + + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + + } + + protected void handlePushLocalComponentFailed(SyncResult result, Long localId) { + Log.e(TAG, "handlePushLocalComponentFailed() >> " + localId); + + try { + + localCollection.removeComponent(localId); + localCollection.commitPendingOperations(); + + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (OperationApplicationException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } + + protected void pushLocallyChangedComponents(SyncResult result) { + Log.d(TAG, "pushLocallyChangedComponents()"); + + try { + + List> updatedIds = localCollection.getUpdatedComponentIds(); + Log.d(TAG, "found + " + updatedIds.size() + " locally updated components"); + + for (Pair componentId : updatedIds) { + try { + + Optional> component = localCollection.getComponent(componentId.second); + + if (component.isPresent()) { + Log.d(TAG, "updating remote component: (" + componentId.first + ", " + componentId.second + ")"); + + remoteCollection.updateHiddenComponent(component.get()); + localCollection.cleanComponent(componentId.first); + localCollection.commitPendingOperations(); + result.stats.numUpdates++; + } + else + Log.e(TAG, "could not get component with id " + componentId.second + " from local collection"); + + } catch (InvalidComponentException e) { + + AbstractDavSyncAdapter.handleException(context, e, result); + handlePushLocalComponentFailed(result, componentId.first); + + } catch (GeneralSecurityException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (DavException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (OperationApplicationException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } + + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } + + + protected abstract void prePushLocallyCreatedComponent(T component); + + protected void pushLocallyCreatedComponents(SyncResult result) { + Log.d(TAG, "pushLocallyCreatedComponents()"); + + try { + + List newIds = localCollection.getNewComponentIds(); + Log.d(TAG, "found + " + newIds.size() + " locally created components"); + + for (Long componentId : newIds) { + try { + + Log.d(TAG, "new local component id >> " + componentId); + String uid = localCollection.populateComponentUid(componentId); + Optional component = localCollection.getComponent(componentId); + + if (component.isPresent()) { + Log.d(TAG, "creating remote component " + componentId); + + prePushLocallyCreatedComponent(component.get()); + remoteCollection.addHiddenComponent(component.get()); + localCollection.cleanComponent(componentId); + localCollection.commitPendingOperations(); + result.stats.numInserts++; + } + else + Log.e(TAG, "could not get component with id " + componentId + + " from local collection"); + + } catch (InvalidComponentException e) { + + AbstractDavSyncAdapter.handleException(context, e, result); + handlePushLocalComponentFailed(result, componentId); + + } catch (GeneralSecurityException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (DavException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (OperationApplicationException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } + + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } + + protected void pullRemotelyCreatedProperties(SyncResult result) { + Log.d(TAG, "pullRemotelyCreatedProperties()"); + + try { + + Optional remoteDisplayName = remoteCollection.getHiddenDisplayName(); + if (remoteDisplayName.isPresent()) { + Optional localDisplayName = localCollection.getDisplayName(); + + if (!localDisplayName.isPresent()) { + Log.d(TAG, "local display name not present, setting using remote"); + localCollection.setDisplayName(remoteDisplayName.get()); + localCollection.commitPendingOperations(); + result.stats.numInserts++; + } + } + + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (OperationApplicationException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (PropertyParseException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (InvalidMacException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (GeneralSecurityException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } + + protected void pullRemotelyChangedProperties(SyncResult result) { + Log.d(TAG, "pullRemotelyChangedProperties()"); + + try { + + if (localCTag.isPresent() && remoteCTag.isPresent() && !localCTag.get().equals(remoteCTag.get())) { + Optional remoteDisplayName = remoteCollection.getHiddenDisplayName(); + if (remoteDisplayName.isPresent()) { + + Optional localDisplayName = localCollection.getDisplayName(); + if (localDisplayName.isPresent() && !localDisplayName.get().equals(remoteDisplayName.get())) { + Log.d(TAG, "local display name present, updating using remote"); + localCollection.setDisplayName(remoteDisplayName.get()); + localCollection.commitPendingOperations(); + result.stats.numUpdates++; + } + } + } + + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (OperationApplicationException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (PropertyParseException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (InvalidMacException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (GeneralSecurityException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } + + // NOTICE: it would be *almost* safe to pull remote ETags only once for all the following methods... + protected void pullRemotelyCreatedComponents(SyncResult result) { + Log.d(TAG, "pullRemotelyCreatedComponents()"); + List> retryList = new LinkedList>(); + + try { + + HashMap remoteETagMap = remoteCollection.getComponentETags(); + Log.d(TAG, "found " + remoteETagMap.size() + " remote components"); + + for (java.util.Map.Entry remoteETagEntry : remoteETagMap.entrySet()) { + try { + + Optional> localComponent = localCollection.getComponent(remoteETagEntry.getKey()); + if (!localComponent.isPresent()) { + + Log.d(TAG, "remote component " + remoteETagEntry.getKey() + " not present locally"); + Optional> remoteComponent = remoteCollection.getHiddenComponent(remoteETagEntry.getKey()); + if (remoteComponent.isPresent()) { + + if (componentHasUid(remoteComponent.get().getComponent())) { + try { + + Log.d(TAG, "creating local component " + remoteETagEntry.getKey() + " using remote"); + localCollection.addComponent(remoteComponent.get()); + localCollection.commitPendingOperations(); + result.stats.numInserts++; + + } catch (InvalidComponentException e) { + Log.w(TAG, "caught invalid component exception. could be a recurrence exception " + + "who's parent has yet to get pulled down."); + retryList.add(remoteComponent.get()); + } + + } + else + throw new InvalidComponentException("remote component is missing UID", true, getNamespace(), + remoteCollection.getPath(), remoteETagEntry.getKey()); + } + else + Log.e(TAG, "remote component " + remoteETagEntry.getKey() + " from etag set not present remotely"); + } + + } catch (InvalidComponentException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (InvalidMacException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (GeneralSecurityException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (DavException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (OperationApplicationException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } + + for (ComponentETagPair retryComponent : retryList) { + try { + + localCollection.addComponent(retryComponent); + localCollection.commitPendingOperations(); + result.stats.numInserts++; + + } catch (InvalidComponentException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (OperationApplicationException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } + + } catch (DavException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } + + protected void pullRemotelyChangedComponents(SyncResult result) { + Log.d(TAG, "pullRemotelyChangedComponents()"); + + try { + + HashMap remoteETagMap = remoteCollection.getComponentETags(); + Log.d(TAG, "found + " + remoteETagMap.size() + " remote components"); + + for (java.util.Map.Entry remoteETagEntry : remoteETagMap.entrySet()) { + try { + + Optional> localComponent = localCollection.getComponent(remoteETagEntry.getKey()); + if (localComponent.isPresent()) { + Log.d(TAG, "remote component " + remoteETagEntry.getKey() + " is present locally"); + + if (!localComponent.get().getETag().isPresent() || + !localComponent.get().getETag().get().equals(remoteETagEntry.getValue())) { + + Optional> remoteComponent = remoteCollection.getHiddenComponent(remoteETagEntry.getKey()); + if (remoteComponent.isPresent()) { + + if (componentHasUid(remoteComponent.get().getComponent())) { + Log.d(TAG, "updating local component " + remoteETagEntry.getKey() + " using remote"); + localCollection.updateComponent(remoteComponent.get()); + localCollection.commitPendingOperations(); + result.stats.numUpdates++; + } else + throw new InvalidComponentException("remote component is missing UID", true, getNamespace(), + remoteCollection.getPath(), remoteETagEntry.getKey()); + } else + Log.e(TAG, "remote component " + remoteETagEntry.getKey() + " from etag set not present remotely"); + } + } + + } catch (InvalidComponentException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (InvalidMacException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (GeneralSecurityException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (DavException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (OperationApplicationException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } + + } catch (DavException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } + + protected void purgeRemotelyDeletedComponents(SyncResult result) { + Log.d(TAG, "pullRemotelyDeletedComponents()"); + + try { + + HashMap localETagMap = localCollection.getComponentETags(); + HashMap remoteETagMap = remoteCollection.getComponentETags(); + + Log.d(TAG, "found " + remoteETagMap.size() + " remote components"); + Log.d(TAG, "found " + localETagMap.size() + " local components"); + + List componentsMissingRemotely = new LinkedList(); + for (java.util.Map.Entry localETagEntry : localETagMap.entrySet()) { + boolean found_remotely = false; + + for (java.util.Map.Entry remoteETagEntry : remoteETagMap.entrySet()) { + if (localETagEntry.getKey().equals(remoteETagEntry.getKey())) + found_remotely = true; + } + + if (!found_remotely) + componentsMissingRemotely.add(localETagEntry.getKey()); + } + + for (String remoteUid : componentsMissingRemotely) { + Log.d(TAG, "deleting local component " + remoteUid + " missing from remote"); + + try { + + localCollection.removeComponent(remoteUid); + localCollection.commitPendingOperations(); + result.stats.numDeletes++; + + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (OperationApplicationException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } + + } catch (DavException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/AbstractLocalComponentCollection.java b/flock/src/main/java/org/anhonesteffort/flock/sync/AbstractLocalComponentCollection.java new file mode 100644 index 0000000..782c558 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/AbstractLocalComponentCollection.java @@ -0,0 +1,302 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync; + +import android.accounts.Account; +import android.content.ContentProviderClient; +import android.content.ContentProviderOperation; +import android.content.ContentUris; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; +import android.util.Log; +import android.util.Pair; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.webdav.InvalidComponentException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; + +/** + * Programmer: rhodey + * Date: 2/4/14 + */ +public abstract class AbstractLocalComponentCollection implements LocalComponentCollection { + + private static final String TAG = "org.anhonesteffort.flock.sync.AbstractLocalComponentCollection"; + + protected ContentProviderClient client; + protected Account account; + protected String remotePath; + protected final Long localId; + + protected ArrayList pendingOperations; + + public AbstractLocalComponentCollection(ContentProviderClient client, + Account account, + String remotePath, + Long localId) + { + this.client = client; + this.account = account; + this.remotePath = remotePath; + this.localId = localId; + + pendingOperations = new ArrayList(); + } + + public Account getAccount() { + return account; + } + + @Override + public String getPath() { + return remotePath; + } + + public Long getLocalId() { + return localId; + } + + protected abstract Uri getSyncAdapterUri(Uri base); + protected abstract Uri getUriForComponents(); + + protected abstract String getColumnNameCollectionLocalId(); + protected abstract String getColumnNameComponentLocalId(); + protected abstract String getColumnNameComponentUid(); + protected abstract String getColumnNameComponentETag(); + + protected abstract String getColumnNameDirty(); + protected abstract String getColumnNameDeleted(); + + public List getNewComponentIds() throws RemoteException { + final String[] PROJECTION = new String[]{getColumnNameComponentLocalId(), getColumnNameComponentUid()}; + final String SELECTION = getColumnNameComponentUid() + " IS NULL AND " + + getColumnNameCollectionLocalId() + "=" + localId; + + Cursor cursor = client.query(getUriForComponents(), PROJECTION, SELECTION, null, null); + List newIds = new LinkedList(); + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + while (cursor.moveToNext()) + newIds.add(cursor.getLong(0)); + cursor.close(); + + return newIds; + } + + public List> getUpdatedComponentIds() throws RemoteException { + final String[] PROJECTION = new String[]{getColumnNameComponentLocalId(), getColumnNameComponentUid()}; + final String SELECTION = getColumnNameDirty() + "=1 AND " + + getColumnNameComponentUid() + " IS NOT NULL AND " + + getColumnNameCollectionLocalId() + "=" + localId; + + Cursor cursor = client.query(getUriForComponents(), PROJECTION, SELECTION, null, null); + List> idPairs = new LinkedList>(); + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + while (cursor.moveToNext()) + idPairs.add(new Pair(cursor.getLong(0), cursor.getString(1))); + cursor.close(); + + return idPairs; + } + + public List> getDeletedComponentIds() throws RemoteException { + final String[] PROJECTION = new String[]{getColumnNameComponentLocalId(), getColumnNameComponentUid()}; + final String SELECTION = getColumnNameDeleted() + "=1 AND " + + getColumnNameComponentUid() + " IS NOT NULL AND " + + getColumnNameCollectionLocalId() + "=" + localId; + + Cursor cursor = client.query(getUriForComponents(), PROJECTION, SELECTION, null, null); + List> idPairs = new LinkedList>(); + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + while (cursor.moveToNext()) + idPairs.add(new Pair(cursor.getLong(0), cursor.getString(1))); + cursor.close(); + + return idPairs; + } + + public List getComponentIds() throws RemoteException { + final String[] PROJECTION = new String[]{getColumnNameComponentLocalId(), getColumnNameComponentUid()}; + final String SELECTION = getColumnNameDeleted() + "=0 AND " + + getColumnNameCollectionLocalId() + "=" + localId; + + Cursor cursor = client.query(getUriForComponents(), PROJECTION, SELECTION, null, null); + List newIds = new LinkedList(); + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + while (cursor.moveToNext()) + newIds.add(cursor.getLong(0)); + cursor.close(); + + return newIds; + } + + public Optional getLocalIdForUid(String uid) throws RemoteException { + final String[] PROJECTION = new String[]{getColumnNameComponentLocalId()}; + final String SELECTION = getColumnNameComponentUid() + "=? AND " + + getColumnNameCollectionLocalId() + "=" + localId; + final String[] SELECTION_ARGS = new String[]{uid}; + + Cursor cursor = client.query(getUriForComponents(), + PROJECTION, + SELECTION, + SELECTION_ARGS, + null); + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + Optional result = Optional.absent(); + if (cursor.moveToNext()) + result = Optional.fromNullable(cursor.getLong(0)); + + cursor.close(); + return result; + } + + public Optional getUidForLocalId(Long localId) throws RemoteException { + final String[] PROJECTION = new String[]{getColumnNameComponentUid()}; + final String SELECTION = getColumnNameComponentLocalId() + "=" + localId + " AND " + + getColumnNameCollectionLocalId() + "=" + this.localId; + + Cursor cursor = client.query(getUriForComponents(), PROJECTION, SELECTION, null, null); + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + Optional result = Optional.absent(); + if (cursor.moveToNext()) + result = Optional.fromNullable(cursor.getString(0)); + + cursor.close(); + return result; + } + + public String populateComponentUid(Long localId) + throws OperationApplicationException, RemoteException + { + Optional uid = getUidForLocalId(localId); + + if (uid.isPresent()) { + Log.d(TAG, "populateComponentUid() uid already exists, ignoring"); + return uid.get(); + } + + String rand = UUID.randomUUID().toString(); + Log.d(TAG, "populateComponentUid() gonna populate " + localId + " with " + rand); + + pendingOperations.add(ContentProviderOperation + .newUpdate(ContentUris.withAppendedId(getUriForComponents(), localId)) + .withValue(getColumnNameComponentUid(), rand) + .withYieldAllowed(false) + .build()); + + commitPendingOperations(); + return rand; + } + + public abstract Optional getComponent(Long localId) throws RemoteException, InvalidComponentException; + + public void removeComponent(Long localId) { + Log.d(TAG, "removeComponent() localId " + localId); + + pendingOperations.add(ContentProviderOperation + .newDelete(ContentUris.withAppendedId(getUriForComponents(), localId)) + .withYieldAllowed(true) + .build()); + } + + @Override + public void removeComponent(String remoteUId) throws RemoteException { + final String SELECTION = getColumnNameComponentUid() + "=? AND " + + getColumnNameCollectionLocalId() + "=" + localId; + final String[] SELECTION_ARGS = new String[]{remoteUId}; + + Log.d(TAG, "removeComponent() remoteUid" + remoteUId); + + pendingOperations.add(ContentProviderOperation + .newDelete(getUriForComponents()) + .withSelection(SELECTION, SELECTION_ARGS) + .withYieldAllowed(true) + .build()); + } + + @Override + public HashMap getComponentETags() throws RemoteException { + final String[] PROJECTION = new String[]{getColumnNameComponentUid(), getColumnNameComponentETag()}; + final String SELECTION = getColumnNameComponentUid() + " IS NOT NULL " + + "AND " + getColumnNameComponentETag() + " IS NOT NULL " + + "AND " + getColumnNameDeleted() + "=0 AND " + + getColumnNameCollectionLocalId() + "=" + localId; + + Cursor cursor = client.query(getUriForComponents(), PROJECTION, SELECTION, null, null); + HashMap pairs = new HashMap(); + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + while (cursor.moveToNext()) + pairs.put(cursor.getString(0), cursor.getString(1)); + cursor.close(); + + return pairs; + } + + public void cleanComponent(Long localId) { + Log.d(TAG, "cleanComponent() localId " + localId); + + pendingOperations.add(ContentProviderOperation + .newUpdate(ContentUris.withAppendedId(getUriForComponents(), localId)) + .withValue(getColumnNameDirty(), 0).build()); + } + + public void dirtyComponent(Long localId) { + Log.d(TAG, "dirtyComponent() localId " + localId); + + pendingOperations.add(ContentProviderOperation + .newUpdate(ContentUris.withAppendedId(getUriForComponents(), localId)) + .withValue(getColumnNameDirty(), 1).build()); + } + + public void commitPendingOperations() throws OperationApplicationException, RemoteException { + Log.d(TAG, "commitPendingOperations()"); + + if (!pendingOperations.isEmpty()) + client.applyBatch(pendingOperations); + pendingOperations.clear(); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/AbstractSyncScheduler.java b/flock/src/main/java/org/anhonesteffort/flock/sync/AbstractSyncScheduler.java new file mode 100644 index 0000000..d024155 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/AbstractSyncScheduler.java @@ -0,0 +1,161 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync; + +import android.accounts.Account; +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.util.Log; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.DavAccountHelper; +import org.anhonesteffort.flock.PreferencesActivity; +import org.anhonesteffort.flock.auth.DavAccount; + +/** + * Programmer: rhodey + */ +public abstract class AbstractSyncScheduler extends ContentObserver { + + private static final String PREFERENCES_NAME = "AbstractSyncScheduler.PREFERENCES_NAME"; + private static final String KEY_SYNC_OVERRIDDEN = "AbstractSyncScheduler.KEY_SYNC_OVERRIDDEN"; + private static final String KEY_TIME_LAST_SYNC = "AbstractSyncScheduler.KEY_TIME_LAST_SYNC"; + + protected Context context; + + public AbstractSyncScheduler(Context context) { + super(null); + this.context = context; + } + + protected abstract String getTAG(); + protected abstract String getAuthority(); + protected abstract Uri getUri(); + + public boolean syncInProgress(Account account) { + return ContentResolver.isSyncActive(account, getAuthority()); + } + + public void registerSelfForBroadcasts() { + context.getContentResolver().unregisterContentObserver(this); + context.getContentResolver().registerContentObserver(getUri(), false, this); + } + + public void requestSync() { + Optional account = DavAccountHelper.getAccount(context); + if (!account.isPresent()) { + Log.e(getTAG(), "account not present, cannot request sync."); + return; + } + + handleInitSyncAdapter(); + + ContentResolver.requestSync(account.get().getOsAccount(), getAuthority(), new Bundle()); + } + + public void setSyncInterval(int minutes) { + long SECONDS_PER_MINUTE = 60L; + long SYNC_INTERVAL = minutes * SECONDS_PER_MINUTE; + + Log.d(getTAG(), "setSyncInterval() " + minutes); + + Optional account = DavAccountHelper.getAccount(context); + if (!account.isPresent()) { + Log.e(getTAG(), "account not present, interval cannot be set"); + return; + } + + if (minutes > 0) { + ContentResolver.addPeriodicSync(account.get().getOsAccount(), + getAuthority(), + new Bundle(), + SYNC_INTERVAL); + } + } + + public void setTimeLastSync(Long timeMilliseconds) { + SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_MULTI_PROCESS); + settings.edit().putLong(KEY_TIME_LAST_SYNC + getAuthority(), timeMilliseconds).commit(); + } + + public Optional getTimeLastSync() { + SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_MULTI_PROCESS); + Long timeMilliseconds = settings.getLong(KEY_TIME_LAST_SYNC + getAuthority(), -1); + + if (timeMilliseconds == -1) + return Optional.absent(); + + return Optional.of(timeMilliseconds); + } + + public void onAccountRemoved() { + Log.d(getTAG(), "onAccountRemoved()"); + SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_MULTI_PROCESS); + settings.edit().putBoolean(KEY_SYNC_OVERRIDDEN + getAuthority(), false).commit(); + } + + private void handleInitSyncAdapter() { + SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_MULTI_PROCESS); + + if (settings.getBoolean(KEY_SYNC_OVERRIDDEN + getAuthority(), false)) + return; + + Optional account = DavAccountHelper.getAccount(context); + if (!account.isPresent()) { + Log.e(getTAG(), "account not present, cannot init sync adapter"); + return; + } + + ContentResolver.setSyncAutomatically(account.get().getOsAccount(), getAuthority(), true); + settings.edit().putBoolean(KEY_SYNC_OVERRIDDEN + getAuthority(), true).commit(); + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + + setSyncInterval(Integer.valueOf( + preferences.getString(PreferencesActivity.KEY_PREF_SYNC_INTERVAL_MINUTES, "60") + )); + } + + @Override + public void onChange(boolean selfChange, Uri changeUri) { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); + Optional account = DavAccountHelper.getAccount(context); + + if (account.isPresent()) + handleInitSyncAdapter(); + + if (settings.getBoolean(PreferencesActivity.KEY_PREF_SYNC_ON_CONTENT_CHANGE, false) && + account.isPresent()) + { + requestSync(); + } + } + + @Override + public void onChange(boolean selfChange) { + onChange(selfChange, null); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/AndroidDavClient.java b/flock/src/main/java/org/anhonesteffort/flock/sync/AndroidDavClient.java new file mode 100644 index 0000000..e1ae2f4 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/AndroidDavClient.java @@ -0,0 +1,62 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync; + +import android.content.Context; +import android.util.Log; + +import org.anhonesteffort.flock.DavAccountHelper; +import org.anhonesteffort.flock.webdav.DavClient; +import org.apache.commons.httpclient.protocol.Protocol; + +import java.net.URL; + +/** + * Programmer: rhodey + */ +public class AndroidDavClient extends DavClient { + + private Context context; + + private void fixClientTrust() { + int port = davHost.getPort(); + if (port < 1) + port = davHost.getDefaultPort(); + + boolean useFlockTrustStore = davHost.toString().equals(OwsWebDav.HREF_WEBDAV_HOST); + AppSecureSocketFactory appSocketFactory = new AppSecureSocketFactory(context, useFlockTrustStore); + Protocol appHttps = new Protocol("https", appSocketFactory, port); + + hostConfiguration.setHost(davHost.getHost(), port, appHttps); + Protocol.registerProtocol("https", appHttps); + } + + public AndroidDavClient(Context context, + URL davHost, + String username, + String password) + { + super(davHost, username, password); + this.context = context; + + if (davHost.getProtocol().equals("https")) + fixClientTrust(); + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/AppSecureSocketFactory.java b/flock/src/main/java/org/anhonesteffort/flock/sync/AppSecureSocketFactory.java new file mode 100644 index 0000000..f1a8dba --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/AppSecureSocketFactory.java @@ -0,0 +1,139 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync; + +import android.content.Context; +import android.content.res.AssetManager; +import android.util.Log; + +import org.apache.commons.httpclient.HttpClientError; +import org.apache.commons.httpclient.params.HttpConnectionParams; +import org.apache.commons.httpclient.protocol.ControllerThreadSocketFactory; +import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.security.KeyStore; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +/** + * Programmer: rhodey + * Date: 3/18/14 + */ +public class AppSecureSocketFactory implements SecureProtocolSocketFactory { + + private static final String TAG = "org.anhonesteffort.flock.sync.AppSecureSocketFactory"; + + private Context appContext; + private boolean useFlockTrustStore; + private SSLContext sslContext; + + public AppSecureSocketFactory(Context context, boolean useFlockTrustStore) { + this.appContext = context; + this.useFlockTrustStore = useFlockTrustStore; + } + + private static SSLContext createAppStoreSSLContext(Context appContext, boolean useFlockTrustStore) + throws HttpClientError + { + if (appContext == null) + throw new HttpClientError("application context is null :("); + + KeyStore trustStore; + + try { + + if (useFlockTrustStore) { + AssetManager assetManager = appContext.getAssets(); + InputStream keyStoreInputStream = assetManager.open("flock.store"); + trustStore = KeyStore.getInstance("BKS"); + + trustStore.load(keyStoreInputStream, "owsflock".toCharArray()); + } + else { + trustStore = KeyStore.getInstance("AndroidCAStore"); + trustStore.load(null, null); + } + + TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); + tmf.init(trustStore); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, tmf.getTrustManagers(), null); + + return sslContext; + + } catch (Exception e) { + Log.e(TAG, "createAppStoreSSLContext() - flock store? " + useFlockTrustStore, e); + throw new HttpClientError(e.toString()); + } + } + + private SSLContext getSSLContext() throws HttpClientError { + if (sslContext == null) + sslContext = createAppStoreSSLContext(appContext, useFlockTrustStore); + + return sslContext; + } + + @Override + public Socket createSocket(String host, int port) + throws HttpClientError, IOException + { + return getSSLContext().getSocketFactory().createSocket(host, port); + } + + @Override + public Socket createSocket(Socket socket, String host, int port, boolean autoClose) + throws HttpClientError, IOException + { + return getSSLContext().getSocketFactory().createSocket(socket, host, port, autoClose); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localAddress, int localPort) + throws HttpClientError, IOException + { + return getSSLContext().getSocketFactory().createSocket(host, port, localAddress, localPort); + } + + @Override + public Socket createSocket(String host, + int port, + InetAddress localAddress, + int localPort, + HttpConnectionParams params) + throws HttpClientError, IOException + { + if (params == null) + return createSocket(host, port, localAddress, localPort); + + int timeout = params.getConnectionTimeout(); + + if (timeout == 0) + return createSocket(host, port, localAddress, localPort); + + return ControllerThreadSocketFactory.createSocket(this, host, port, localAddress, localPort, timeout); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/HidingDavCollection.java b/flock/src/main/java/org/anhonesteffort/flock/sync/HidingDavCollection.java new file mode 100644 index 0000000..ccfa4f5 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/HidingDavCollection.java @@ -0,0 +1,71 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.crypto.InvalidMacException; +import org.anhonesteffort.flock.webdav.ComponentETagPair; +import org.anhonesteffort.flock.webdav.InvalidComponentException; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.jackrabbit.webdav.DavException; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.HashMap; +import java.util.List; + +/** + * Programmer: rhodey + */ +public interface HidingDavCollection { + + public String getPath(); + + public Optional getCTag() throws PropertyParseException; + + public Optional getHiddenDisplayName() + throws PropertyParseException, InvalidMacException, GeneralSecurityException, IOException; + + public void setHiddenDisplayName(String displayName) + throws DavException, IOException, InvalidMacException, GeneralSecurityException; + + public Optional getEncryptedKeyMaterial() throws PropertyParseException; + + public void setEncryptedKeyMaterial(String encryptedKeyMaterial) throws DavException, IOException; + + public HashMap getComponentETags() throws DavException, IOException; + + public Optional> getHiddenComponent(String uid) + throws InvalidComponentException, DavException, + InvalidMacException, GeneralSecurityException, IOException; + + public List> getHiddenComponents() + throws InvalidComponentException, DavException, + InvalidMacException, GeneralSecurityException, IOException; + + public void addHiddenComponent(T component) + throws InvalidComponentException, DavException, GeneralSecurityException, IOException; + + public void updateHiddenComponent(ComponentETagPair component) + throws InvalidComponentException, DavException, GeneralSecurityException, IOException; + + public void removeComponent(String path) throws DavException, IOException; + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/HidingDavCollectionMixin.java b/flock/src/main/java/org/anhonesteffort/flock/sync/HidingDavCollectionMixin.java new file mode 100644 index 0000000..30bab09 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/HidingDavCollectionMixin.java @@ -0,0 +1,118 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.crypto.InvalidMacException; +import org.anhonesteffort.flock.crypto.MasterCipher; +import org.anhonesteffort.flock.webdav.AbstractDavComponentCollection; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +/** + * Programmer: rhodey + */ +public class HidingDavCollectionMixin { + + protected static final String PROPERTY_NAME_KEY_MATERIAL_SALT = "X-KEY-MATERIAL-SALT"; + protected static final String PROPERTY_NAME_ENCRYPTED_KEY_MATERIAL = "X-ENCRYPTED-KEY-MATERIAL"; + + protected static final DavPropertyName PROPERTY_KEY_MATERIAL_SALT = DavPropertyName.create( + PROPERTY_NAME_KEY_MATERIAL_SALT, + OwsWebDav.NAMESPACE + ); + protected static final DavPropertyName PROPERTY_ENCRYPTED_KEY_MATERIAL = DavPropertyName.create( + PROPERTY_NAME_ENCRYPTED_KEY_MATERIAL, + OwsWebDav.NAMESPACE + ); + + private AbstractDavComponentCollection collection; + private MasterCipher masterCipher; + + public HidingDavCollectionMixin(AbstractDavComponentCollection collection, + MasterCipher masterCipher) + { + this.collection = collection; + this.masterCipher = masterCipher; + } + + public DavPropertyNameSet getPropertyNamesForFetch() { + DavPropertyNameSet hidingCollectionProps = new DavPropertyNameSet(); + hidingCollectionProps.add(PROPERTY_KEY_MATERIAL_SALT); + hidingCollectionProps.add(PROPERTY_ENCRYPTED_KEY_MATERIAL); + + return hidingCollectionProps; + } + + public Optional getKeyMaterialSalt() throws PropertyParseException { + return collection.getProperty(PROPERTY_KEY_MATERIAL_SALT, String.class); + } + + public void setKeyMaterialSalt(String keyMaterialSalt) + throws DavException, IOException + { + DavPropertySet updateProperties = new DavPropertySet(); + updateProperties.add(new DefaultDavProperty(PROPERTY_KEY_MATERIAL_SALT, + keyMaterialSalt)); + + collection.patchProperties(updateProperties, new DavPropertyNameSet()); + } + + public Optional getEncryptedKeyMaterial() throws PropertyParseException { + return collection.getProperty(PROPERTY_ENCRYPTED_KEY_MATERIAL, String.class); + } + + public void setEncryptedKeyMaterial(String encryptedKeyMaterial) + throws DavException, IOException + { + DavPropertySet updateProperties = new DavPropertySet(); + updateProperties.add(new DefaultDavProperty(PROPERTY_ENCRYPTED_KEY_MATERIAL, + encryptedKeyMaterial)); + + collection.patchProperties(updateProperties, new DavPropertyNameSet()); + } + + public Optional getHiddenDisplayName() + throws PropertyParseException, InvalidMacException, + GeneralSecurityException, IOException + { + Optional displayName = collection.getDisplayName(); + + if (displayName.isPresent()) + return Optional.of(HidingUtil.decodeAndDecryptIfNecessary(masterCipher, displayName.get())); + + return Optional.absent(); + } + + public void setHiddenDisplayName(String displayName) + throws DavException, IOException, + InvalidMacException, GeneralSecurityException + { + collection.setDisplayName(HidingUtil.encryptEncodeAndPrefix(masterCipher, displayName)); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/HidingDavStore.java b/flock/src/main/java/org/anhonesteffort/flock/sync/HidingDavStore.java new file mode 100644 index 0000000..91a3b24 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/HidingDavStore.java @@ -0,0 +1,47 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.jackrabbit.webdav.DavException; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.List; + +/** + * Programmer: rhodey + */ +public interface HidingDavStore> { + + public String getHostHREF(); + + public Optional getCollection(String path) throws DavException, IOException; + + public List getCollections() throws PropertyParseException, DavException, IOException; + + public void addCollection(String path) throws DavException, IOException, GeneralSecurityException; + + public void removeCollection(String path) throws DavException, IOException; + + public void releaseConnections(); + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/HidingUtil.java b/flock/src/main/java/org/anhonesteffort/flock/sync/HidingUtil.java new file mode 100644 index 0000000..c2c7403 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/HidingUtil.java @@ -0,0 +1,79 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync; + +import org.anhonesteffort.flock.crypto.InvalidMacException; +import org.anhonesteffort.flock.crypto.MasterCipher; +import org.anhonesteffort.flock.util.Util; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.Arrays; + +/** + * Programmer: rhodey + */ +public class HidingUtil { + + private static final byte[] PREFIX_ENCRYPTED_DATA = new byte[] {0x23, 0x23, 0x23, 0x24, 0x24, 0x24}; + + private static boolean hasEncryptedDataPrefix(byte[] data) { + if (data.length <= PREFIX_ENCRYPTED_DATA.length) + return false; + + boolean matches = true; + for (int i = 0; i < PREFIX_ENCRYPTED_DATA.length; i++) { + if (data[i] != PREFIX_ENCRYPTED_DATA[i]) + matches = false; + } + + return matches; + } + + public static byte[] encryptEncodeAndPrefix(MasterCipher masterCipher, byte[] data) + throws IOException, GeneralSecurityException + { + return Util.combine(PREFIX_ENCRYPTED_DATA, masterCipher.encryptAndEncode(data)); + } + + public static String encryptEncodeAndPrefix(MasterCipher masterCipher, String data) + throws IOException, GeneralSecurityException + { + return new String(encryptEncodeAndPrefix(masterCipher, data.getBytes())); + } + + public static byte[] decodeAndDecryptIfNecessary(MasterCipher masterCipher, byte[] data) + throws InvalidMacException, IOException, GeneralSecurityException + { + if (!hasEncryptedDataPrefix(data)) + return data; + + byte[] encodedIvCiphertextAndMac = Arrays.copyOfRange(data, PREFIX_ENCRYPTED_DATA.length, data.length); + + return masterCipher.decodeAndDecrypt(encodedIvCiphertextAndMac); + } + + public static String decodeAndDecryptIfNecessary(MasterCipher masterCipher, String data) + throws InvalidMacException, IOException, GeneralSecurityException + { + return new String(decodeAndDecryptIfNecessary(masterCipher, data.getBytes())); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/InvalidLocalComponentException.java b/flock/src/main/java/org/anhonesteffort/flock/sync/InvalidLocalComponentException.java new file mode 100644 index 0000000..d37c0e7 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/InvalidLocalComponentException.java @@ -0,0 +1,85 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync; + +import org.anhonesteffort.flock.webdav.InvalidComponentException; +import org.apache.jackrabbit.webdav.xml.Namespace; + +/** + * Programmer: rhodey + */ +public class InvalidLocalComponentException extends InvalidComponentException { + + private long localId; + + public InvalidLocalComponentException(String message, + Namespace namespace, + String path, + long localId) + { + super(message, false, namespace, path); + + this.localId = localId; + } + + public InvalidLocalComponentException(String message, + Namespace namespace, + String path, + long localId, + Throwable cause) + { + super(message, false, namespace, path, cause); + + this.localId = localId; + } + + public InvalidLocalComponentException(String message, + Namespace namespace, + String path, + long localId, + String uid) + { + super(message, false, namespace, path, uid); + + this.localId = localId; + } + + public InvalidLocalComponentException(String message, + Namespace namespace, + String path, + long localId, + String uid, + Throwable cause) + { + super(message, false, namespace, path, uid, cause); + + this.localId = localId; + } + + public Long getLocalId() { + return localId; + } + + @Override + public String toString() { + return super.toString() + ", local id: " + localId; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/LocalComponentCollection.java b/flock/src/main/java/org/anhonesteffort/flock/sync/LocalComponentCollection.java new file mode 100644 index 0000000..9124fc4 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/LocalComponentCollection.java @@ -0,0 +1,58 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync; + +import android.os.RemoteException; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.webdav.ComponentETagPair; +import org.anhonesteffort.flock.webdav.InvalidComponentException; + +import java.util.HashMap; +import java.util.List; + +/** + * Programmer: rhodey + */ +public interface LocalComponentCollection { + + public String getPath(); + + public Optional getCTag() throws RemoteException; + + public void setCTag(String ctag) throws RemoteException; + + public Optional getDisplayName() throws RemoteException; + + public void setDisplayName(String displayName) throws RemoteException; + + public HashMap getComponentETags() throws RemoteException; + + public Optional> getComponent(String uid) throws RemoteException, InvalidComponentException; + + public List> getComponents() throws RemoteException, InvalidComponentException; + + public void addComponent(ComponentETagPair component) throws RemoteException, InvalidComponentException; + + public void updateComponent(ComponentETagPair component) throws RemoteException, InvalidComponentException; + + public void removeComponent(String path) throws RemoteException; + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/LocalComponentStore.java b/flock/src/main/java/org/anhonesteffort/flock/sync/LocalComponentStore.java new file mode 100644 index 0000000..9ae0abb --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/LocalComponentStore.java @@ -0,0 +1,43 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync; + +import android.os.RemoteException; + +import com.google.common.base.Optional; + +import java.util.List; + +/** + * Programmer: rhodey + */ +public interface LocalComponentStore> { + + public Optional getCollection(String path) throws RemoteException; + + public List getCollections() throws RemoteException; + + public void addCollection(String path) throws RemoteException; + + public void addCollection(String path, String displayName) throws RemoteException; + + public void removeCollection(String path) throws RemoteException; + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/OwsWebDav.java b/flock/src/main/java/org/anhonesteffort/flock/sync/OwsWebDav.java new file mode 100644 index 0000000..203751e --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/OwsWebDav.java @@ -0,0 +1,43 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync; + +import org.apache.jackrabbit.webdav.xml.Namespace; + +/** + * Programmer: rhodey + */ +public class OwsWebDav { + + public static final String WEBDAV_HOST = "flock-sync.whispersystems.org"; + public static final int WEBDAV_PORT = 443; + public static final String HREF_WEBDAV_HOST = "https://" + WEBDAV_HOST + ":" + WEBDAV_PORT; + + public static final int STATUS_PAYMENT_REQUIRED = 402; + + public static final Namespace NAMESPACE = Namespace.getNamespace("org.anhonesteffort.flock"); + + public static String getAddressbookPathForUsername(String username) { + return "/addressbooks/__uids__/" + username + "/addressbook/"; + } + +} + + diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/SyncBooter.java b/flock/src/main/java/org/anhonesteffort/flock/sync/SyncBooter.java new file mode 100644 index 0000000..4bdbf82 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/SyncBooter.java @@ -0,0 +1,42 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import org.anhonesteffort.flock.sync.addressbook.AddressbookSyncScheduler; +import org.anhonesteffort.flock.sync.calendar.CalendarsSyncScheduler; +import org.anhonesteffort.flock.sync.key.KeySyncScheduler; + +/** + * Programmer: rhodey + */ +public class SyncBooter extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + new KeySyncScheduler(context).registerSelfForBroadcasts(); + new AddressbookSyncScheduler(context).registerSelfForBroadcasts(); + new CalendarsSyncScheduler(context).registerSelfForBroadcasts(); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/AddressbookSyncScheduler.java b/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/AddressbookSyncScheduler.java new file mode 100644 index 0000000..07649da --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/AddressbookSyncScheduler.java @@ -0,0 +1,54 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.addressbook; + +import android.content.Context; +import android.net.Uri; +import android.provider.ContactsContract; + +import org.anhonesteffort.flock.sync.AbstractSyncScheduler; + +/** + * Programmer: rhodey + */ +public class AddressbookSyncScheduler extends AbstractSyncScheduler { + + private static final String TAG = "org.anhonesteffort.flock.sync.addressbook.AddressbookSyncScheduler"; + public static final String CONTENT_AUTHORITY = ContactsContract.AUTHORITY; + + public AddressbookSyncScheduler(Context context) { + super(context); + } + + @Override + protected Uri getUri() { + return ContactsContract.Contacts.CONTENT_URI; + } + + @Override + protected String getTAG() { + return TAG; + } + + public String getAuthority() { + return CONTENT_AUTHORITY; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/AddressbookSyncService.java b/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/AddressbookSyncService.java new file mode 100644 index 0000000..d2dbb92 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/AddressbookSyncService.java @@ -0,0 +1,131 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.addressbook; + +import android.accounts.Account; +import android.app.Service; +import android.content.ContentProviderClient; +import android.content.Context; +import android.content.Intent; +import android.content.SyncResult; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.DavAccountHelper; +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.crypto.KeyHelper; +import org.anhonesteffort.flock.crypto.MasterCipher; +import org.anhonesteffort.flock.sync.AbstractDavSyncAdapter; +import org.anhonesteffort.flock.sync.key.KeySyncScheduler; +import org.apache.jackrabbit.webdav.DavException; + +import java.io.IOException; +import java.util.Date; + +/** + * Programmer: rhodey + */ +public class AddressbookSyncService extends Service { + + private static final String TAG = "org.anhonesteffort.flock.sync.addressbook.AddressbookSyncService"; + + private static ContactsSyncAdapter sSyncAdapter = null; + private static final Object sSyncAdapterLock = new Object(); + + @Override + public void onCreate() { + synchronized (sSyncAdapterLock) { + if (sSyncAdapter == null) + sSyncAdapter = new ContactsSyncAdapter(getApplicationContext()); + } + } + + @Override + public IBinder onBind(Intent intent) { + return sSyncAdapter.getSyncAdapterBinder(); + } + + private static class ContactsSyncAdapter extends AbstractDavSyncAdapter { + + public ContactsSyncAdapter(Context context) { + super(context); + } + + protected String getAuthority() { + return AddressbookSyncScheduler.CONTENT_AUTHORITY; + } + + @Override + public void onPerformSync(Account account, + Bundle extras, + String authority, + ContentProviderClient provider, + SyncResult syncResult) + { + Log.d(TAG, "performing sync for authority: " + authority + ", account: " + account.name); + + Optional davAccount = DavAccountHelper.getAccount(getContext()); + if (!davAccount.isPresent()) { + Log.d(TAG, "dav account is missing"); + syncResult.stats.numAuthExceptions++; + showNotifications(syncResult); + return; + } + + try { + + Optional masterCipher = KeyHelper.getMasterCipher(getContext()); + if (!masterCipher.isPresent()) { + Log.d(TAG, "master cipher is missing"); + syncResult.stats.numAuthExceptions++; + return ; + } + + LocalAddressbookStore localStore = new LocalAddressbookStore(getContext(), provider, davAccount.get()); + HidingCardDavStore remoteStore = DavAccountHelper.getHidingCardDavStore(getContext(), davAccount.get(), masterCipher.get()); + + for (LocalContactCollection localCollection : localStore.getCollections()) { + Log.d(TAG, "found local collection: " + localCollection.getPath()); + Optional remoteCollection = remoteStore.getCollection(localCollection.getPath()); + + if (remoteCollection.isPresent()) + new AddressbookSyncWorker(getContext(), localCollection, remoteCollection.get()).run(syncResult, false); + else { + Log.d(TAG, "local collection missing remotely, deleting locally"); + localStore.removeCollection(localCollection.getPath()); + } + } + + remoteStore.releaseConnections(); + + } catch (IOException e) { + handleException(getContext(), e, syncResult); + } catch (DavException e) { + handleException(getContext(), e, syncResult); + } + + showNotifications(syncResult); + new AddressbookSyncScheduler(getContext()).setTimeLastSync(new Date().getTime()); + } + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/AddressbookSyncWorker.java b/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/AddressbookSyncWorker.java new file mode 100644 index 0000000..9ba07da --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/AddressbookSyncWorker.java @@ -0,0 +1,64 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.addressbook; + +import android.content.Context; +import android.util.Log; + +import net.fortuna.ical4j.model.Calendar; +import net.fortuna.ical4j.model.Property; +import net.fortuna.ical4j.model.component.VEvent; + +import ezvcard.VCard; + +import org.anhonesteffort.flock.webdav.carddav.CardDavConstants; +import org.anhonesteffort.flock.sync.AbstractDavSyncWorker; +import org.apache.jackrabbit.webdav.xml.Namespace; + +/** + * Programmer: rhodey + */ +public class AddressbookSyncWorker extends AbstractDavSyncWorker { + + private static final String TAG = "org.anhonesteffort.flock.sync.addressbook.AddressbookSyncWorker"; + + protected AddressbookSyncWorker(Context context, + LocalContactCollection localCollection, + HidingCardDavCollection remoteCollection) + { + super(context, localCollection, remoteCollection); + } + + @Override + protected Namespace getNamespace() { + return CardDavConstants.CARDDAV_NAMESPACE; + } + + @Override + protected boolean componentHasUid(VCard component) { + return component.getUid() != null && component.getUid().getValue() != null; + } + + @Override + protected void prePushLocallyCreatedComponent(VCard component) { + + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/ContactCopiedListener.java b/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/ContactCopiedListener.java new file mode 100644 index 0000000..b0621c7 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/ContactCopiedListener.java @@ -0,0 +1,33 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.addressbook; + +import android.accounts.Account; + +/** + * Programmer: rhodey + */ +public interface ContactCopiedListener { + + public void onContactCopied(Account fromAccount, Account toAccount); + + public void onContactCopyFailed(Exception e, Account fromAccount, Account toAccount); + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/ContactFactory.java b/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/ContactFactory.java new file mode 100644 index 0000000..d85b509 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/ContactFactory.java @@ -0,0 +1,1246 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.addressbook; + +import android.content.ContentProviderClient; +import android.content.ContentValues; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; +import android.provider.ContactsContract; +import android.util.Log; + +import com.google.common.base.Optional; +import ezvcard.VCard; +import ezvcard.VCardVersion; +import ezvcard.parameter.AddressType; +import ezvcard.parameter.EmailType; +import ezvcard.parameter.ImageType; +import ezvcard.parameter.ImppType; +import ezvcard.parameter.TelephoneType; +import ezvcard.property.Address; +import ezvcard.property.Birthday; +import ezvcard.property.Email; +import ezvcard.property.FormattedName; +import ezvcard.property.Impp; +import ezvcard.property.Nickname; +import ezvcard.property.Note; +import ezvcard.property.Organization; +import ezvcard.property.Photo; +import ezvcard.property.RawProperty; +import ezvcard.property.Role; +import ezvcard.property.StructuredName; +import ezvcard.property.Telephone; +import ezvcard.property.Uid; +import ezvcard.property.Url; +import ezvcard.util.IOUtils; + +import org.anhonesteffort.flock.webdav.carddav.CardDavConstants; +import org.anhonesteffort.flock.webdav.ComponentETagPair; +import org.anhonesteffort.flock.webdav.InvalidComponentException; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.WordUtils; + +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; + +/** + * Programmer: rhodey + * + * Much thanks to the DAVDroid project and especially + * Richard Hirner (bitfire web engineering) for leading + * the way in shoving VCard objects into Androids Contacts + * Content Provider. This would have been much more of a + * pain without a couple hints from the DAVDroid codebase. + */ +public class ContactFactory { + + private static final String TAG = "org.anhonesteffort.flock.sync.addressbook.ContactFactory"; + + private static final String PROPERTY_PHONETIC_GIVEN_NAME = "X-PHONETIC-GIVEN-NAME"; + private static final String PROPERTY_PHONETIC_MIDDLE_NAME = "X-PHONETIC-MIDDLE-NAME"; + private static final String PROPERTY_PHONETIC_FAMILY_NAME = "X-PHONETIC-FAMILY-NAME"; + + private static final EmailType EMAIL_TYPE_MOBILE = EmailType.get("X-MOBILE"); + + private static final String PROPERTY_SIP = "X-SIP"; + private static final String PROPERTY_STARRED = "X-STARRED"; + private static final String PROPERTY_EVENT_ANNIVERSARY = "X-EVENT-ANNIVERSARY"; + private static final String PROPERTY_EVENT_OTHER = "X-EVENT-OTHER"; + private static final String PROPERTY_EVENT_CUSTOM = "X-EVENT-CUSTOM"; + private static final String PARAMETER_EVENT_CUSTOM_LABEL = "X-EVENT-CUSTOM-LABEL"; + + private static String propertyNameToLabel(String propertyName) { + return WordUtils.capitalize(propertyName.toLowerCase().replace("x-", "").replace("_", " ")); + } + + protected static String labelToPropertyName(String label) { + return "X-" + label.replace(" ","_").toUpperCase(); + } + + protected static String[] getProjectionForRawContact() { + return new String[] { + ContactsContract.RawContacts._ID, + ContactsContract.RawContacts.SOURCE_ID, // UID + ContactsContract.RawContacts.SYNC1, // ETAG + ContactsContract.RawContacts.STARRED + }; + } + + protected static ContentValues getValuesForRawContact(Cursor cursor) { + ContentValues values = new ContentValues(4); + + values.put(ContactsContract.RawContacts._ID, cursor.getLong(0)); + values.put(ContactsContract.RawContacts.SOURCE_ID, cursor.getString(1)); + values.put(ContactsContract.RawContacts.SYNC1, cursor.getString(2)); + values.put(ContactsContract.RawContacts.STARRED, (cursor.getInt(3) != 0)); + + return values; + } + + protected static ContentValues getValuesForRawContact(ComponentETagPair vCard) { + ContentValues values = new ContentValues(); + Uid uid = vCard.getComponent().getUid(); + + if (uid != null) + values.put(ContactsContract.RawContacts.SOURCE_ID, uid.getValue()); + + if (vCard.getETag().isPresent()) + values.put(ContactsContract.RawContacts.SYNC1, vCard.getETag().get()); + + RawProperty starredProp = vCard.getComponent().getExtendedProperty(PROPERTY_STARRED); + if (starredProp != null) { + boolean is_starred = Integer.parseInt(starredProp.getValue()) != 0; + values.put(ContactsContract.RawContacts.STARRED, is_starred); + } + + return values; + } + + protected static ComponentETagPair getVCard(ContentValues rawContactValues) { + String uidText = rawContactValues.getAsString(ContactsContract.RawContacts.SOURCE_ID); + String eTagText = rawContactValues.getAsString(ContactsContract.RawContacts.SYNC1); + Boolean starred = rawContactValues.getAsBoolean(ContactsContract.RawContacts.STARRED); + + VCard vCard = new VCard(); + vCard.setVersion(VCardVersion.V3_0); + + vCard.setUid(new Uid(uidText)); + + if (starred != null) + vCard.setExtendedProperty(PROPERTY_STARRED, starred ? "1" : "0"); + + Optional eTag = Optional.fromNullable(eTagText); + + return new ComponentETagPair(vCard, eTag); + } + + protected static String[] getProjectionForStructuredName() { + return new String[] { + ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, // 00 + ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, // 01 + ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, // 02 + ContactsContract.CommonDataKinds.StructuredName.PREFIX, // 03 + ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, // 04 + ContactsContract.CommonDataKinds.StructuredName.SUFFIX, // 05 + ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME, // 06 + ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME, // 07 + ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME // 08 + }; + } + + protected static ContentValues getValuesForStructuredName(Cursor cursor) { + ContentValues values = new ContentValues(9); + + values.put(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, cursor.getString(0)); + values.put(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, cursor.getString(1)); + values.put(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, cursor.getString(2)); + values.put(ContactsContract.CommonDataKinds.StructuredName.PREFIX, cursor.getString(3)); + values.put(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, cursor.getString(4)); + values.put(ContactsContract.CommonDataKinds.StructuredName.SUFFIX, cursor.getString(5)); + values.put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME, cursor.getString(6)); + values.put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME, cursor.getString(7)); + values.put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME, cursor.getString(8)); + + return values; + } + + protected static Optional getValuesForStructuredName(VCard vCard) { + if (vCard.getStructuredName() != null) { + ContentValues values = new ContentValues(); + + values.put(ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE); + + FormattedName formattedName = vCard.getFormattedName(); + if (formattedName != null) + values.put(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, + formattedName.getValue()); + + StructuredName structuredName = vCard.getStructuredName(); + if (structuredName != null) { + values.put(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, + structuredName.getGiven()); + values.put(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, + structuredName.getFamily()); + + if (structuredName.getPrefixes().size() > 0) + values.put(ContactsContract.CommonDataKinds.StructuredName.PREFIX, + structuredName.getPrefixes().toString()); + + if (structuredName.getAdditional().size() > 0) + values.put(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, + StringUtils.join(structuredName.getAdditional(), " ")); + + if (structuredName.getSuffixes().size() > 0) + values.put(ContactsContract.CommonDataKinds.StructuredName.SUFFIX, + structuredName.getSuffixes().toString()); + } + + RawProperty phoneticGivenName = vCard.getExtendedProperty(PROPERTY_PHONETIC_GIVEN_NAME); + if (phoneticGivenName != null) + values.put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME, + phoneticGivenName.getValue()); + + RawProperty phoneticMiddleName = vCard.getExtendedProperty(PROPERTY_PHONETIC_MIDDLE_NAME); + if (phoneticMiddleName != null) + values.put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME, + phoneticMiddleName.getValue()); + + RawProperty phoneticFamilyName = vCard.getExtendedProperty(PROPERTY_PHONETIC_FAMILY_NAME); + if (phoneticFamilyName != null) + values.put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME, + phoneticFamilyName.getValue()); + + return Optional.of(values); + } + + // TODO: handle case where only email address is present + Log.w(TAG, "structured name missing, returning absent"); + return Optional.absent(); + } + + protected static void addStructuredName(VCard vCard, ContentValues structuredNameValues) { + String displayName = structuredNameValues.getAsString(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME); + String givenName = structuredNameValues.getAsString(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME); + String familyName = structuredNameValues.getAsString(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME); + String prefix = structuredNameValues.getAsString(ContactsContract.CommonDataKinds.StructuredName.PREFIX); + String middleName = structuredNameValues.getAsString(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME); + String suffix = structuredNameValues.getAsString(ContactsContract.CommonDataKinds.StructuredName.SUFFIX); + String phoneticGivenName = structuredNameValues.getAsString(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME); + String phoneticMiddleName = structuredNameValues.getAsString(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME); + String phoneticFamilyName = structuredNameValues.getAsString(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME); + + if (displayName != null) { + FormattedName formattedName = new FormattedName(displayName); + vCard.addFormattedName(formattedName); + + StructuredName structuredName = new StructuredName(); + + if (givenName != null) + structuredName.setGiven(givenName); + + if (familyName != null) + structuredName.setFamily(familyName); + + if (prefix != null) + structuredName.addPrefix(prefix); + + if (middleName != null) + structuredName.addAdditional(middleName); + + if (suffix != null) + structuredName.addSuffix(suffix); + + vCard.setStructuredName(structuredName); + + if (phoneticGivenName != null) + vCard.addExtendedProperty(PROPERTY_PHONETIC_GIVEN_NAME, phoneticGivenName); + + if (phoneticMiddleName != null) + vCard.addExtendedProperty(PROPERTY_PHONETIC_MIDDLE_NAME, phoneticMiddleName); + + if (phoneticFamilyName != null) + vCard.addExtendedProperty(PROPERTY_PHONETIC_FAMILY_NAME, phoneticFamilyName); + } + else + Log.w(TAG, "display name missing, nothing to add"); + } + + protected static String[] getProjectionForPhoneNumber() { + return new String[] { + ContactsContract.CommonDataKinds.Phone.TYPE, // 00 + ContactsContract.CommonDataKinds.Phone.LABEL, // 01 + ContactsContract.CommonDataKinds.Phone.NUMBER, // 02 + ContactsContract.CommonDataKinds.Phone.IS_PRIMARY, // 03 + ContactsContract.CommonDataKinds.Phone.IS_SUPER_PRIMARY // 04 + }; + } + + protected static ContentValues getValuesForPhoneNumber(Cursor cursor) { + ContentValues values = new ContentValues(5); + + values.put(ContactsContract.CommonDataKinds.Phone.TYPE, cursor.getInt(0)); + values.put(ContactsContract.CommonDataKinds.Phone.LABEL, cursor.getString(1)); + values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, cursor.getString(2)); + values.put(ContactsContract.CommonDataKinds.Phone.IS_PRIMARY, (cursor.getInt(3) != 0)); + values.put(ContactsContract.CommonDataKinds.Phone.IS_SUPER_PRIMARY, (cursor.getInt(4) != 0)); + + return values; + } + + protected static List getValuesForPhoneNumbers(VCard vCard) { + List valuesList = new LinkedList(); + List telephones = vCard.getTelephoneNumbers(); + + for (Telephone telephone : telephones) { + ContentValues values = new ContentValues(); + int phone_type = ContactsContract.CommonDataKinds.Phone.TYPE_OTHER; + + values.put(ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE); + + if (telephone.getTypes().contains(TelephoneType.FAX)) { + if (telephone.getTypes().contains(TelephoneType.HOME)) + phone_type = ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME; + else if (telephone.getTypes().contains(TelephoneType.WORK)) + phone_type = ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK; + else + phone_type = ContactsContract.CommonDataKinds.Phone.TYPE_OTHER_FAX; + } + else if (telephone.getTypes().contains(TelephoneType.CELL)) + phone_type = ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE; + else if (telephone.getTypes().contains(TelephoneType.PAGER)) + phone_type = ContactsContract.CommonDataKinds.Phone.TYPE_PAGER; + else if (telephone.getTypes().contains(TelephoneType.WORK)) + phone_type = ContactsContract.CommonDataKinds.Phone.TYPE_WORK; + else if (telephone.getTypes().contains(TelephoneType.HOME)) + phone_type = ContactsContract.CommonDataKinds.Phone.TYPE_HOME; + else if (telephone.getTypes().contains(TelephoneType.PREF)) + phone_type = ContactsContract.CommonDataKinds.Phone.TYPE_MAIN; + else if (!telephone.getTypes().isEmpty()) { + phone_type = ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM; + values.put(ContactsContract.CommonDataKinds.Phone.LABEL, + propertyNameToLabel(telephone.getTypes().iterator().next().getValue())); + } + + values.put(ContactsContract.CommonDataKinds.Phone.TYPE, phone_type); + values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, telephone.getText()); + + values.put(ContactsContract.CommonDataKinds.Phone.IS_PRIMARY, + telephone.getTypes().contains(TelephoneType.PREF) ? 1 : 0); + values.put(ContactsContract.CommonDataKinds.Phone.IS_SUPER_PRIMARY, + telephone.getTypes().contains(TelephoneType.PREF) ? 1 : 0); + + valuesList.add(values); + } + + return valuesList; + } + + protected static void addPhoneNumber(String path, VCard vCard, ContentValues phoneNumberValues) + throws InvalidComponentException + { + Integer type = phoneNumberValues.getAsInteger(ContactsContract.CommonDataKinds.Phone.TYPE); + String label = phoneNumberValues.getAsString(ContactsContract.CommonDataKinds.Phone.LABEL); + String number = phoneNumberValues.getAsString(ContactsContract.CommonDataKinds.Phone.NUMBER); + Boolean isPrimary = phoneNumberValues.getAsBoolean(ContactsContract.CommonDataKinds.Phone.IS_PRIMARY); + Boolean isSuperPrimary = phoneNumberValues.getAsBoolean(ContactsContract.CommonDataKinds.Phone.IS_SUPER_PRIMARY); + + if (type != null && number != null) { + Telephone telephone = new Telephone(number); + + switch (type) { + case ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE: + telephone.addType(TelephoneType.CELL); + break; + + case ContactsContract.CommonDataKinds.Phone.TYPE_WORK: + telephone.addType(TelephoneType.WORK); + break; + + case ContactsContract.CommonDataKinds.Phone.TYPE_HOME: + telephone.addType(TelephoneType.HOME); + break; + + case ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK: + telephone.addType(TelephoneType.FAX); + telephone.addType(TelephoneType.WORK); + break; + + case ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME: + telephone.addType(TelephoneType.FAX); + telephone.addType(TelephoneType.HOME); + break; + + case ContactsContract.CommonDataKinds.Phone.TYPE_PAGER: + telephone.addType(TelephoneType.PAGER); + break; + + case ContactsContract.CommonDataKinds.Phone.TYPE_MAIN: + telephone.addType(TelephoneType.PREF); + break; + + default: + if (label != null) + telephone.addType(TelephoneType.get(labelToPropertyName(label))); + } + + if (isPrimary != null && isPrimary) + telephone.addType(TelephoneType.PREF); + else if (isSuperPrimary != null && isSuperPrimary) + telephone.addType(TelephoneType.PREF); + + vCard.addTelephoneNumber(telephone); + } + else { + Log.e(TAG, "phone type or number is null, not adding anything"); + throw new InvalidComponentException("phone type or number is null", false, + CardDavConstants.CARDDAV_NAMESPACE, path); + } + } + + protected static String[] getProjectionForEmailAddress() { + return new String[] { + ContactsContract.CommonDataKinds.Email.TYPE, // 00 + ContactsContract.CommonDataKinds.Email.ADDRESS, // 01 + ContactsContract.CommonDataKinds.Email.LABEL, // 02 + ContactsContract.CommonDataKinds.Email.IS_PRIMARY, // 03 + ContactsContract.CommonDataKinds.Email.IS_SUPER_PRIMARY // 04 + }; + } + + protected static ContentValues getValuesForEmailAddress(Cursor cursor) { + ContentValues values = new ContentValues(5); + + values.put(ContactsContract.CommonDataKinds.Email.TYPE, cursor.getInt(0)); + values.put(ContactsContract.CommonDataKinds.Email.ADDRESS, cursor.getString(1)); + values.put(ContactsContract.CommonDataKinds.Email.LABEL, cursor.getString(2)); + values.put(ContactsContract.CommonDataKinds.Email.IS_PRIMARY, (cursor.getInt(3) != 0)); + values.put(ContactsContract.CommonDataKinds.Email.IS_SUPER_PRIMARY, (cursor.getInt(4) != 0)); + + return values; + } + + protected static List getValuesForEmailAddresses(VCard vCard) { + List valuesList = new LinkedList(); + List emails = vCard.getEmails(); + + for (Email email : emails) { + ContentValues values = new ContentValues(); + + values.put(ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE); + + if (email.getTypes().contains(EmailType.HOME)) + values.put(ContactsContract.CommonDataKinds.Email.TYPE, + ContactsContract.CommonDataKinds.Email.TYPE_HOME); + else if (email.getTypes().contains(EmailType.WORK)) + values.put(ContactsContract.CommonDataKinds.Email.TYPE, + ContactsContract.CommonDataKinds.Email.TYPE_WORK); + else if (email.getTypes().contains(EMAIL_TYPE_MOBILE)) + values.put(ContactsContract.CommonDataKinds.Email.TYPE, + ContactsContract.CommonDataKinds.Email.TYPE_MOBILE); + else if (!email.getTypes().isEmpty()) { + values.put(ContactsContract.CommonDataKinds.Email.TYPE, + ContactsContract.CommonDataKinds.Email.TYPE_CUSTOM); + values.put(ContactsContract.CommonDataKinds.Email.LABEL, + propertyNameToLabel(email.getTypes().iterator().next().getValue())); + } + else + values.put(ContactsContract.CommonDataKinds.Email.TYPE, + ContactsContract.CommonDataKinds.Email.TYPE_OTHER); + + values.put(ContactsContract.CommonDataKinds.Email.ADDRESS, email.getValue()); + + values.put(ContactsContract.CommonDataKinds.Email.IS_PRIMARY, + email.getTypes().contains(EmailType.PREF) ? 1 : 0); + values.put(ContactsContract.CommonDataKinds.Email.IS_SUPER_PRIMARY, + email.getTypes().contains(EmailType.PREF) ? 1 : 0); + + valuesList.add(values); + } + + return valuesList; + } + + protected static void addEmailAddress(String path, VCard vCard, ContentValues emailValues) + throws InvalidComponentException + { + Integer type = emailValues.getAsInteger(ContactsContract.CommonDataKinds.Email.TYPE); + String label = emailValues.getAsString(ContactsContract.CommonDataKinds.Email.LABEL); + String address = emailValues.getAsString(ContactsContract.CommonDataKinds.Email.ADDRESS); + Boolean isPrimary = emailValues.getAsBoolean(ContactsContract.CommonDataKinds.Email.IS_PRIMARY); + Boolean isSuperPrimary = emailValues.getAsBoolean(ContactsContract.CommonDataKinds.Email.IS_SUPER_PRIMARY); + + if (type != null && address != null) { + Email email = new Email(address); + + switch (type) { + case ContactsContract.CommonDataKinds.Email.TYPE_HOME: + email.addType(EmailType.HOME); + break; + + case ContactsContract.CommonDataKinds.Email.TYPE_WORK: + email.addType(EmailType.WORK); + break; + + case ContactsContract.CommonDataKinds.Email.TYPE_MOBILE: + email.addType(EMAIL_TYPE_MOBILE); + break; + + default: + if (label != null) + email.addType(EmailType.get(label)); + break; + } + + if (isPrimary != null && isPrimary) + email.addType(EmailType.PREF); + else if (isSuperPrimary != null && isSuperPrimary) + email.addType(EmailType.PREF); + + vCard.addEmail(email); + } + else { + Log.e(TAG, "email type or address is null, not adding anything"); + throw new InvalidComponentException("email type or address is null", false, + CardDavConstants.CARDDAV_NAMESPACE, path); + } + } + + protected static String[] getProjectionForPicture() { + return new String[] { + ContactsContract.CommonDataKinds.Photo.RAW_CONTACT_ID, // 00 + ContactsContract.CommonDataKinds.Photo.SYNC1 // 01 (image type) + }; + } + + protected static ContentValues getValuesForPicture(Cursor cursor) { + ContentValues values = new ContentValues(2); + + values.put(ContactsContract.CommonDataKinds.Photo.RAW_CONTACT_ID, cursor.getLong(0)); + values.put(ContactsContract.CommonDataKinds.Photo.SYNC1, cursor.getString(1)); + + return values; + } + + protected static Optional getValuesForPicture(VCard vCard) { + if (vCard.getPhotos().size() > 0) { + try { + + ContentValues values = new ContentValues(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(vCard.getPhotos().get(0).getData()); + + values.put(ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE); + values.put(ContactsContract.CommonDataKinds.Photo.PHOTO, + vCard.getPhotos().get(0).getData()); + + return Optional.of(values); + + } catch (IOException e) { + Log.e(TAG, "caught exception while shoving photo into content values", e); + } + } + else + Log.w(TAG, "no photos found in vcard, returning absent"); + + return Optional.absent(); + } + + protected static void addPicture(String path, + ContentProviderClient client, + Uri pictureUri, + VCard vCard) + throws InvalidComponentException, RemoteException + { + try { + + AssetFileDescriptor fileDescriptor = client.openAssetFile(pictureUri, "r"); + InputStream inputStream = fileDescriptor.createInputStream(); + + Photo photo = new Photo(IOUtils.toByteArray(inputStream), ImageType.JPEG); + vCard.addPhoto(photo); + + } catch (FileNotFoundException e) { + // nothing to do... + } catch (IOException e) { + throw new InvalidComponentException("caught exception while adding picture", false, + CardDavConstants.CARDDAV_NAMESPACE, path); + } + } + + protected static String[] getProjectionForOrganization() { + return new String[] { + ContactsContract.CommonDataKinds.Organization.COMPANY, // 00 + ContactsContract.CommonDataKinds.Organization.TITLE // 01 + }; + } + + protected static ContentValues getValuesForOrganization(Cursor cursor) { + ContentValues values = new ContentValues(2); + + values.put(ContactsContract.CommonDataKinds.Organization.COMPANY, cursor.getString(0)); + values.put(ContactsContract.CommonDataKinds.Organization.TITLE, cursor.getString(1)); + + return values; + } + + protected static List getValuesForOrganization(VCard vCard) { + List valuesList = new LinkedList(); + + for (int i = 0; i < vCard.getOrganizations().size(); i++) { + Organization organization = vCard.getOrganizations().get(i); + ContentValues values = new ContentValues(); + + values.put(ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE); + + values.put(ContactsContract.CommonDataKinds.Organization.COMPANY, + organization.getValues().get(0)); + + if (vCard.getRoles().size() > i) + values.put(ContactsContract.CommonDataKinds.Organization.TITLE, + vCard.getRoles().get(i).getValue()); + + valuesList.add(values); + } + + return valuesList; + } + + protected static void addOrganizer(VCard vCard, ContentValues organizerValues) { + String companyText = organizerValues.getAsString(ContactsContract.CommonDataKinds.Organization.COMPANY); + String roleText = organizerValues.getAsString(ContactsContract.CommonDataKinds.Organization.TITLE); + + if (companyText != null) { + Organization organization = new Organization(); + organization.addValue(companyText); + vCard.addOrganization(organization); + } + + if (roleText != null) { + Role role = new Role(roleText); + vCard.addRole(role); + } + } + + protected static String[] getProjectionForInstantMessaging() { + return new String[] { + ContactsContract.CommonDataKinds.Im.TYPE, // 00 + ContactsContract.CommonDataKinds.Im.DATA, // 01 + ContactsContract.CommonDataKinds.Im.LABEL, // 02 + ContactsContract.CommonDataKinds.Im.PROTOCOL, // 03 + ContactsContract.CommonDataKinds.Im.CUSTOM_PROTOCOL // 04 + }; + } + + protected static ContentValues getValuesForInstantMessaging(Cursor cursor) { + ContentValues values = new ContentValues(5); + + values.put(ContactsContract.CommonDataKinds.Im.TYPE, cursor.getInt(0)); + values.put(ContactsContract.CommonDataKinds.Im.DATA, cursor.getString(1)); + values.put(ContactsContract.CommonDataKinds.Im.LABEL, cursor.getString(2)); + values.put(ContactsContract.CommonDataKinds.Im.PROTOCOL, cursor.getInt(3)); + values.put(ContactsContract.CommonDataKinds.Im.CUSTOM_PROTOCOL, cursor.getString(4)); + + return values; + } + + protected static List getValuesForInstantMessaging(VCard vCard) { + List valuesList = new LinkedList(); + + for (Impp messenger : vCard.getImpps()) { + if (messenger.getProtocol() == null || messenger.getProtocol().equalsIgnoreCase("sip")) + break; + + ContentValues values = new ContentValues(); + + values.put(ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE); + values.put(ContactsContract.CommonDataKinds.Im.TYPE, + ContactsContract.CommonDataKinds.Im.TYPE_OTHER); + + values.put(ContactsContract.CommonDataKinds.Im.DATA, messenger.getHandle()); + + if (messenger.isAim()) + values.put(ContactsContract.CommonDataKinds.Im.PROTOCOL, + ContactsContract.CommonDataKinds.Im.PROTOCOL_AIM); + else if (messenger.isMsn()) + values.put(ContactsContract.CommonDataKinds.Im.PROTOCOL, + ContactsContract.CommonDataKinds.Im.PROTOCOL_MSN); + else if (messenger.isYahoo()) + values.put(ContactsContract.CommonDataKinds.Im.PROTOCOL, + ContactsContract.CommonDataKinds.Im.PROTOCOL_YAHOO); + else if (messenger.isSkype()) + values.put(ContactsContract.CommonDataKinds.Im.PROTOCOL, + ContactsContract.CommonDataKinds.Im.PROTOCOL_SKYPE); + else if (messenger.isIcq()) + values.put(ContactsContract.CommonDataKinds.Im.PROTOCOL, + ContactsContract.CommonDataKinds.Im.PROTOCOL_ICQ); + else if (messenger.isXmpp()) + values.put(ContactsContract.CommonDataKinds.Im.PROTOCOL, + ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER); + else if (messenger.getProtocol().equalsIgnoreCase("jabber")) + values.put(ContactsContract.CommonDataKinds.Im.PROTOCOL, + ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER); + else if (messenger.getProtocol().equalsIgnoreCase("qq")) + values.put(ContactsContract.CommonDataKinds.Im.PROTOCOL, + ContactsContract.CommonDataKinds.Im.PROTOCOL_QQ); + else if (messenger.getProtocol().equalsIgnoreCase("google-talk")) + values.put(ContactsContract.CommonDataKinds.Im.PROTOCOL, + ContactsContract.CommonDataKinds.Im.PROTOCOL_GOOGLE_TALK); + else if (messenger.getProtocol().equalsIgnoreCase("netmeeting")) + values.put(ContactsContract.CommonDataKinds.Im.PROTOCOL, + ContactsContract.CommonDataKinds.Im.PROTOCOL_NETMEETING); + else { + values.put(ContactsContract.CommonDataKinds.Im.PROTOCOL, + ContactsContract.CommonDataKinds.Im.PROTOCOL_CUSTOM); + values.put(ContactsContract.CommonDataKinds.Im.CUSTOM_PROTOCOL, + messenger.getProtocol()); + } + + valuesList.add(values); + } + + return valuesList; + } + + protected static void addInstantMessaging(String path, VCard vCard, ContentValues imValues) + throws InvalidComponentException + { + Integer type = imValues.getAsInteger(ContactsContract.CommonDataKinds.Im.TYPE); + Integer protocol = imValues.getAsInteger(ContactsContract.CommonDataKinds.Im.PROTOCOL); + String customProtocol = imValues.getAsString(ContactsContract.CommonDataKinds.Im.CUSTOM_PROTOCOL); + String handle = imValues.getAsString(ContactsContract.CommonDataKinds.Im.DATA); + + if (type != null && protocol != null && handle != null) { + Impp impp; + + switch (protocol) { + case ContactsContract.CommonDataKinds.Im.PROTOCOL_AIM: + impp = Impp.aim(handle); + break; + + case ContactsContract.CommonDataKinds.Im.PROTOCOL_MSN: + impp = Impp.msn(handle); + break; + + case ContactsContract.CommonDataKinds.Im.PROTOCOL_YAHOO: + impp = Impp.yahoo(handle); + break; + + case ContactsContract.CommonDataKinds.Im.PROTOCOL_SKYPE: + impp = Impp.skype(handle); + break; + + case ContactsContract.CommonDataKinds.Im.PROTOCOL_ICQ: + impp = Impp.icq(handle); + break; + + case ContactsContract.CommonDataKinds.Im.PROTOCOL_QQ: + impp = new Impp("qq", handle); + break; + + case ContactsContract.CommonDataKinds.Im.PROTOCOL_GOOGLE_TALK: + impp = new Impp("google-talk", handle); + break; + + case ContactsContract.CommonDataKinds.Im.PROTOCOL_NETMEETING: + impp = new Impp("netmeeting", handle); + break; + + case ContactsContract.CommonDataKinds.Im.PROTOCOL_CUSTOM: + impp = new Impp(customProtocol, handle); + break; + + default: + impp = Impp.xmpp(handle); + break; + } + + impp.addType(ImppType.PERSONAL); + + vCard.addImpp(impp); + } + else { + Log.e(TAG, "im type, protocol, or handle is null, not adding anything"); + throw new InvalidComponentException("im type, protocol, or handle is null", false, + CardDavConstants.CARDDAV_NAMESPACE, path); + } + } + + protected static String[] getProjectionForNickName() { + return new String[] { + ContactsContract.CommonDataKinds.Nickname.NAME // 00 + }; + } + + protected static ContentValues getValuesForNickName(Cursor cursor) { + ContentValues values = new ContentValues(1); + + values.put(ContactsContract.CommonDataKinds.Nickname.NAME, cursor.getString(0)); + + return values; + } + + protected static List getValuesForNickName(VCard vCard) { + List valuesList = new LinkedList(); + + for (Nickname nickname : vCard.getNicknames()) { + ContentValues values = new ContentValues(); + + values.put(ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE); + values.put(ContactsContract.CommonDataKinds.Nickname.NAME, + nickname.getValues().get(0)); + + valuesList.add(values); + } + + return valuesList; + } + + protected static void addNickName(VCard vCard, ContentValues nickNameValues) { + String nickNameText = nickNameValues.getAsString(ContactsContract.CommonDataKinds.Nickname.NAME); + + if (nickNameText != null) { + Nickname nickname = new Nickname(); + nickname.addValue(nickNameText); + + vCard.addNickname(nickname); + } + } + + protected static String[] getProjectionForNote() { + return new String[] { + ContactsContract.CommonDataKinds.Note.NOTE // 00 + }; + } + + protected static ContentValues getValuesForNote(Cursor cursor) { + ContentValues values = new ContentValues(1); + + values.put(ContactsContract.CommonDataKinds.Note.NOTE, cursor.getString(0)); + + return values; + } + + protected static List getValuesForNote(VCard vCard) { + List valuesList = new LinkedList(); + + for (Note note : vCard.getNotes()) { + ContentValues values = new ContentValues(); + + values.put(ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE); + values.put(ContactsContract.CommonDataKinds.Note.NOTE, + note.getValue()); + + valuesList.add(values); + } + + return valuesList; + } + + protected static void addNote(VCard vCard, ContentValues noteValues) { + String noteText = noteValues.getAsString(ContactsContract.CommonDataKinds.Note.NOTE); + + if (noteText != null) { + Note note = new Note(noteText); + vCard.addNote(note); + } + } + + protected static String[] getProjectionForPostalAddress() { + return new String[] { + ContactsContract.CommonDataKinds.StructuredPostal.TYPE, // 00 + ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS, // 01 + ContactsContract.CommonDataKinds.StructuredPostal.LABEL, // 02 + ContactsContract.CommonDataKinds.StructuredPostal.STREET, // 03 + ContactsContract.CommonDataKinds.StructuredPostal.POBOX, // 04 + ContactsContract.CommonDataKinds.StructuredPostal.NEIGHBORHOOD, // 05 + ContactsContract.CommonDataKinds.StructuredPostal.CITY, // 06 + ContactsContract.CommonDataKinds.StructuredPostal.REGION, // 07 + ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE, // 08 + ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY // 09 + }; + } + + protected static ContentValues getValuesForPostalAddress(Cursor cursor) { + ContentValues values = new ContentValues(10); + + values.put(ContactsContract.CommonDataKinds.StructuredPostal.TYPE, cursor.getInt(0)); + values.put(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS, cursor.getString(1)); + values.put(ContactsContract.CommonDataKinds.StructuredPostal.LABEL, cursor.getString(2)); + values.put(ContactsContract.CommonDataKinds.StructuredPostal.STREET, cursor.getString(3)); + values.put(ContactsContract.CommonDataKinds.StructuredPostal.POBOX, cursor.getString(4)); + values.put(ContactsContract.CommonDataKinds.StructuredPostal.NEIGHBORHOOD, cursor.getString(5)); + values.put(ContactsContract.CommonDataKinds.StructuredPostal.CITY, cursor.getString(6)); + values.put(ContactsContract.CommonDataKinds.StructuredPostal.REGION, cursor.getString(7)); + values.put(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE, cursor.getString(8)); + values.put(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY, cursor.getString(9)); + + return values; + } + + protected static List getValuesForPostalAddresses(VCard vCard) { + List valuesList = new LinkedList(); + + for (ezvcard.property.Address address : vCard.getAddresses()) { + ContentValues values = new ContentValues(); + + values.put(ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE); + + if (address.getTypes().contains(AddressType.HOME)) + values.put(ContactsContract.CommonDataKinds.StructuredPostal.TYPE, + ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME); + else if (address.getTypes().contains(AddressType.WORK)) + values.put(ContactsContract.CommonDataKinds.StructuredPostal.TYPE, + ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK); + else if (!address.getTypes().isEmpty()) { + values.put(ContactsContract.CommonDataKinds.StructuredPostal.TYPE, + ContactsContract.CommonDataKinds.StructuredPostal.TYPE_CUSTOM); + values.put(ContactsContract.CommonDataKinds.StructuredPostal.LABEL, + propertyNameToLabel(address.getTypes().iterator().next().getValue())); + } + else + values.put(ContactsContract.CommonDataKinds.StructuredPostal.TYPE, + ContactsContract.CommonDataKinds.StructuredPostal.TYPE_OTHER); + + String formattedAddress = address.getLabel(); + if (StringUtils.isEmpty(address.getLabel())) { + String lineStreet = StringUtils.join( + new String[] { address.getStreetAddress(), address.getPoBox(), address.getExtendedAddress() }, + " "); + + String 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"); + } + values.put(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS, formattedAddress); + + values.put(ContactsContract.CommonDataKinds.StructuredPostal.STREET, address.getStreetAddress()); + values.put(ContactsContract.CommonDataKinds.StructuredPostal.POBOX, address.getPoBox()); + values.put(ContactsContract.CommonDataKinds.StructuredPostal.NEIGHBORHOOD, address.getExtendedAddress()); + values.put(ContactsContract.CommonDataKinds.StructuredPostal.CITY, address.getLocality()); + values.put(ContactsContract.CommonDataKinds.StructuredPostal.REGION, address.getRegion()); + values.put(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE, address.getPostalCode()); + values.put(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY, address.getCountry()); + + valuesList.add(values); + } + + return valuesList; + } + + protected static void addPostalAddress(String path, VCard vCard, ContentValues addressValues) + throws InvalidComponentException + { + Integer addressType = addressValues.getAsInteger(ContactsContract.CommonDataKinds.StructuredPostal.TYPE); + String formattedAddress = addressValues.getAsString(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS); + String label = addressValues.getAsString(ContactsContract.CommonDataKinds.StructuredPostal.LABEL); + String street = addressValues.getAsString(ContactsContract.CommonDataKinds.StructuredPostal.STREET); + String poBox = addressValues.getAsString(ContactsContract.CommonDataKinds.StructuredPostal.POBOX); + String neighborhood = addressValues.getAsString(ContactsContract.CommonDataKinds.StructuredPostal.NEIGHBORHOOD); + String city = addressValues.getAsString(ContactsContract.CommonDataKinds.StructuredPostal.CITY); + String region = addressValues.getAsString(ContactsContract.CommonDataKinds.StructuredPostal.REGION); + String postcode = addressValues.getAsString(ContactsContract.CommonDataKinds.StructuredPostal.POBOX); + String country = addressValues.getAsString(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY); + + if (addressType != null && formattedAddress != null) { + Address address = new Address(); + address.setLabel(formattedAddress); + + switch (addressType) { + case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME: + address.addType(AddressType.HOME); + break; + + case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK: + address.addType(AddressType.WORK); + break; + + case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_CUSTOM: + if (label != null) + AddressType.get(label); + break; + } + + if (street != null) + address.setStreetAddress(street); + + if (poBox != null) + address.setPoBox(poBox); + + if (neighborhood != null) + address.setExtendedAddress(neighborhood); + + if (city != null) + address.setLocality(city); + + if (region != null) + address.setRegion(region); + + if (postcode != null) + address.setPostalCode(postcode); + + if (country != null) + address.setCountry(country); + + vCard.addAddress(address); + } + else { + Log.e(TAG, "im address type or formatted address is null, not adding anything"); + throw new InvalidComponentException("im address type or formatted address is null", false, + CardDavConstants.CARDDAV_NAMESPACE, path); + } + } + + protected static String[] getProjectionForWebsite() { + return new String[] { + ContactsContract.CommonDataKinds.Website.URL // 00 + }; + } + + protected static ContentValues getValuesForWebsite(Cursor cursor) { + ContentValues values = new ContentValues(1); + + values.put(ContactsContract.CommonDataKinds.Website.URL, cursor.getString(0)); + + return values; + } + + protected static List getValuesForWebsites(VCard vCard) { + List valuesList = new LinkedList(); + + for (Url url : vCard.getUrls()) { + ContentValues values = new ContentValues(); + + values.put(ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE); + values.put(ContactsContract.CommonDataKinds.Website.URL, url.getValue()); + + valuesList.add(values); + } + + return valuesList; + } + + protected static void addWebsite(String path, VCard vCard, ContentValues websiteValues) + throws InvalidComponentException + { + String urlText = websiteValues.getAsString(ContactsContract.CommonDataKinds.Website.URL); + + if (urlText != null) { + Url url = new Url(urlText); + vCard.addUrl(url); + } else { + Log.e(TAG, "url is null, not adding anything"); + throw new InvalidComponentException("url is null", false, + CardDavConstants.CARDDAV_NAMESPACE, path); + } + } + + protected static String[] getProjectionForEvent() { + return new String[] { + ContactsContract.CommonDataKinds.Event.TYPE, // 00 + ContactsContract.CommonDataKinds.Event.LABEL, // 01 + ContactsContract.CommonDataKinds.Event.START_DATE // 02 + }; + } + + protected static ContentValues getValuesForEvent(Cursor cursor) { + ContentValues values = new ContentValues(3); + + values.put(ContactsContract.CommonDataKinds.Event.TYPE, cursor.getInt(0)); + values.put(ContactsContract.CommonDataKinds.Event.LABEL, cursor.getString(1)); + values.put(ContactsContract.CommonDataKinds.Event.START_DATE, cursor.getString(2)); + + return values; + } + + protected static List getValuesForEvents(VCard vCard) { + List valuesList = new LinkedList(); + + if (vCard.getBirthday() != null && vCard.getBirthday().getDate() != null) { + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + ContentValues values = new ContentValues(); + + values.put(ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE); + values.put(ContactsContract.CommonDataKinds.Event.TYPE, + ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY); + + values.put(ContactsContract.CommonDataKinds.Event.START_DATE, + formatter.format(vCard.getBirthday().getDate())); + + valuesList.add(values); + } + + if (vCard.getExtendedProperty(PROPERTY_EVENT_ANNIVERSARY) != null) { + ContentValues values = new ContentValues(); + + values.put(ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE); + values.put(ContactsContract.CommonDataKinds.Event.TYPE, + ContactsContract.CommonDataKinds.Event.TYPE_ANNIVERSARY); + values.put(ContactsContract.CommonDataKinds.Event.START_DATE, + vCard.getExtendedProperty(PROPERTY_EVENT_ANNIVERSARY).getValue()); + + valuesList.add(values); + } + + if (vCard.getExtendedProperty(PROPERTY_EVENT_OTHER) != null) { + ContentValues values = new ContentValues(); + + values.put(ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE); + values.put(ContactsContract.CommonDataKinds.Event.TYPE, + ContactsContract.CommonDataKinds.Event.TYPE_OTHER); + values.put(ContactsContract.CommonDataKinds.Event.START_DATE, + vCard.getExtendedProperty(PROPERTY_EVENT_OTHER).getValue()); + + valuesList.add(values); + } + + if (vCard.getExtendedProperty(PROPERTY_EVENT_CUSTOM) != null) { + ContentValues values = new ContentValues(); + + values.put(ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE); + values.put(ContactsContract.CommonDataKinds.Event.TYPE, + ContactsContract.CommonDataKinds.Event.TYPE_CUSTOM); + values.put(ContactsContract.CommonDataKinds.Event.LABEL, + vCard.getExtendedProperty(PROPERTY_EVENT_CUSTOM).getParameter(PARAMETER_EVENT_CUSTOM_LABEL)); + values.put(ContactsContract.CommonDataKinds.Event.START_DATE, + vCard.getExtendedProperty(PROPERTY_EVENT_CUSTOM).getValue()); + + valuesList.add(values); + } + + return valuesList; + } + + protected static void addEvent(String path, VCard vCard, ContentValues eventValues) + throws InvalidComponentException + { + Integer eventType = eventValues.getAsInteger(ContactsContract.CommonDataKinds.Event.TYPE); + String eventLabel = eventValues.getAsString(ContactsContract.CommonDataKinds.Event.LABEL); + String eventStartDate = eventValues.getAsString(ContactsContract.CommonDataKinds.Event.START_DATE); + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + + if (eventType != null && eventStartDate != null) { + try { + + if (eventType == ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY) { + Birthday birthday = new Birthday(formatter.parse(eventStartDate)); + vCard.setBirthday(birthday); + } + + } catch (ParseException e) { + throw new InvalidComponentException("caught exception while parsing birthday", false, + CardDavConstants.CARDDAV_NAMESPACE, path); + } + + if (eventType == ContactsContract.CommonDataKinds.Event.TYPE_ANNIVERSARY) + vCard.setExtendedProperty(PROPERTY_EVENT_ANNIVERSARY, eventStartDate); + else if (eventType == ContactsContract.CommonDataKinds.Event.TYPE_OTHER) + vCard.setExtendedProperty(PROPERTY_EVENT_OTHER, eventStartDate); + else if (eventType == ContactsContract.CommonDataKinds.Event.TYPE_CUSTOM) + vCard.setExtendedProperty(PROPERTY_EVENT_CUSTOM, eventStartDate) + .setParameter(PARAMETER_EVENT_CUSTOM_LABEL, eventLabel); + } + else { + Log.e(TAG, "event type or event start date is null, not adding anything"); + throw new InvalidComponentException("event type or event start date is null", false, + CardDavConstants.CARDDAV_NAMESPACE, path); + } + } + + protected static String[] getProjectionForSipAddress() { + return new String[] { + ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS, // 00 + ContactsContract.CommonDataKinds.SipAddress.TYPE, // 01 + ContactsContract.CommonDataKinds.SipAddress.LABEL // 02 + }; + } + + protected static ContentValues getValuesForSipAddress(Cursor cursor) { + ContentValues values = new ContentValues(3); + + values.put(ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS, cursor.getString(0)); + values.put(ContactsContract.CommonDataKinds.SipAddress.TYPE, cursor.getInt(1)); + values.put(ContactsContract.CommonDataKinds.SipAddress.LABEL, cursor.getString(2)); + + return values; + } + + protected static List getValuesForSipAddresses(VCard vCard) { + List valuesList = new LinkedList(); + + for (RawProperty sipAddress : vCard.getExtendedProperties(PROPERTY_SIP)) { + ContentValues values = new ContentValues(); + + values.put(ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE); + + values.put(ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS, + sipAddress.getValue()); + + values.put(ContactsContract.CommonDataKinds.SipAddress.TYPE, + ContactsContract.CommonDataKinds.SipAddress.TYPE_OTHER); + + valuesList.add(values); + } + + return valuesList; + } + + protected static void addSipAddress(VCard vCard, ContentValues sipAddressValues) { + String sipAddress = sipAddressValues.getAsString(ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS); + + if (sipAddress != null) + vCard.setExtendedProperty(PROPERTY_SIP, sipAddress); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/HidingCardDavCollection.java b/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/HidingCardDavCollection.java new file mode 100644 index 0000000..87640bb --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/HidingCardDavCollection.java @@ -0,0 +1,240 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.addressbook; + +import android.util.Log; + +import com.google.common.base.Optional; +import ezvcard.Ezvcard; +import ezvcard.VCard; +import ezvcard.parameter.ImageType; +import ezvcard.property.Photo; +import ezvcard.property.RawProperty; +import ezvcard.property.StructuredName; + +import org.anhonesteffort.flock.webdav.carddav.CardDavConstants; +import org.anhonesteffort.flock.crypto.InvalidMacException; +import org.anhonesteffort.flock.crypto.MasterCipher; +import org.anhonesteffort.flock.sync.HidingDavCollection; +import org.anhonesteffort.flock.sync.HidingDavCollectionMixin; +import org.anhonesteffort.flock.sync.HidingUtil; +import org.anhonesteffort.flock.webdav.ComponentETagPair; +import org.anhonesteffort.flock.webdav.InvalidComponentException; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.anhonesteffort.flock.webdav.carddav.CardDavCollection; +import org.anhonesteffort.flock.webdav.carddav.CardDavStore; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.LinkedList; +import java.util.List; + +/** + * Programmer: rhodey + */ +public class HidingCardDavCollection extends CardDavCollection implements HidingDavCollection { + + private static final String TAG = "org.anhonesteffort.flock.sync.addressbook.HidingCardDavCollection"; + + private static final String PROPERTY_NAME_FLOCK_HIDDEN = "X-FLOCK-HIDDEN"; + private static final String PARAMETER_NAME_FLOCK_HIDDEN_PHOTO = "X-FLOCK-HIDDEN-PHOTO"; + + private MasterCipher masterCipher; + private HidingDavCollectionMixin delegate; + + protected HidingCardDavCollection(CardDavStore cardDavStore, + String path, + MasterCipher masterCipher) + { + super(cardDavStore, path); + + this.masterCipher = masterCipher; + this.delegate = new HidingDavCollectionMixin(this, masterCipher); + } + + protected HidingCardDavCollection(CardDavCollection cardDavCollection, + MasterCipher masterCipher) + { + super((CardDavStore) cardDavCollection.getStore(), + cardDavCollection.getPath(), + cardDavCollection.getProperties()); + + this.masterCipher = masterCipher; + this.delegate = new HidingDavCollectionMixin(this, masterCipher); + } + + @Override + protected DavPropertyNameSet getPropertyNamesForFetch() { + DavPropertyNameSet addressbookProps = super.getPropertyNamesForFetch(); + DavPropertyNameSet hidingProps = delegate.getPropertyNamesForFetch(); + + addressbookProps.addAll(hidingProps); + return addressbookProps; + } + + @Override + public Optional getHiddenDisplayName() + throws PropertyParseException, InvalidMacException, + GeneralSecurityException, IOException + { + return delegate.getHiddenDisplayName(); + } + + @Override + public void setHiddenDisplayName(String displayName) + throws DavException, IOException, + InvalidMacException, GeneralSecurityException + { + delegate.setHiddenDisplayName(displayName); + } + + @Override + public Optional getEncryptedKeyMaterial() throws PropertyParseException { + return delegate.getEncryptedKeyMaterial(); + } + + @Override + public void setEncryptedKeyMaterial(String encryptedKeyMaterial) + throws DavException, IOException + { + delegate.setEncryptedKeyMaterial(encryptedKeyMaterial); + } + + @Override + public Optional> getHiddenComponent(String uid) + throws InvalidComponentException, DavException, + InvalidMacException, GeneralSecurityException, IOException + { + Optional> originalComponentPair = super.getComponent(uid); + if (!originalComponentPair.isPresent()) + return Optional.absent(); + + VCard exposedVCard = originalComponentPair.get().getComponent(); + RawProperty protectedVCard = exposedVCard.getExtendedProperty(PROPERTY_NAME_FLOCK_HIDDEN); + + if (protectedVCard == null) + return originalComponentPair; + + String recoveredVCardText = HidingUtil.decodeAndDecryptIfNecessary(masterCipher, protectedVCard.getValue()); + VCard recoveredVCard = Ezvcard.parse(recoveredVCardText.replace("\\n", "\n")).first(); + + if (exposedVCard.getPhotos().size() > 0) { + Photo protectedPhoto = exposedVCard.getPhotos().get(0); + String parameterFlockHiddenPhoto = protectedPhoto.getParameter(PARAMETER_NAME_FLOCK_HIDDEN_PHOTO); + if (parameterFlockHiddenPhoto != null && parameterFlockHiddenPhoto.equals("true")) { + byte[] recoveredPhotoData = HidingUtil.decodeAndDecryptIfNecessary(masterCipher, protectedPhoto.getData()); + Photo recoveredPhoto = new Photo(recoveredPhotoData, ImageType.JPEG); + recoveredVCard.addPhoto(recoveredPhoto); + } + else + Log.e(TAG, "received vcard a photo missing the " + PARAMETER_NAME_FLOCK_HIDDEN_PHOTO + " parameter."); + } + return Optional.of(new ComponentETagPair(recoveredVCard, + originalComponentPair.get().getETag())); + } + + @Override + public List> getHiddenComponents() + throws InvalidComponentException, DavException, + InvalidMacException, GeneralSecurityException, IOException + { + List> exposedComponentPairs = super.getComponents(); + List> recoveredComponentPairs = new LinkedList>(); + + for (ComponentETagPair exposedComponentPair : exposedComponentPairs) { + VCard exposedVCard = exposedComponentPair.getComponent(); + RawProperty protectedVCard = exposedVCard.getExtendedProperty(PROPERTY_NAME_FLOCK_HIDDEN); + + if (protectedVCard == null) + recoveredComponentPairs.add(exposedComponentPair); + + else { + String recoveredVCardText = HidingUtil.decodeAndDecryptIfNecessary(masterCipher, protectedVCard.getValue()); + VCard recoveredVCard = Ezvcard.parse(recoveredVCardText.replace("\\n", "\n")).first(); + + if (exposedVCard.getPhotos().size() > 0) { + Photo protectedPhoto = exposedVCard.getPhotos().get(0); + if (protectedPhoto.getParameter(PARAMETER_NAME_FLOCK_HIDDEN_PHOTO).equals("true")) { + byte[] recoveredPhotoData = HidingUtil.decodeAndDecryptIfNecessary(masterCipher, protectedPhoto.getData()); + Photo recoveredPhoto = new Photo(recoveredPhotoData, ImageType.JPEG); + recoveredVCard.addPhoto(recoveredPhoto); + } + } + recoveredComponentPairs.add(new ComponentETagPair(recoveredVCard, + exposedComponentPair.getETag())); + } + } + + return recoveredComponentPairs; + } + + protected void putHiddenComponentToServer(VCard exposedVCard, Optional ifMatchETag) + throws InvalidComponentException, GeneralSecurityException, IOException, DavException + { + if (exposedVCard.getUid() == null) + throw new InvalidComponentException("Cannot put a VCard to server without UID!", false, + CardDavConstants.CARDDAV_NAMESPACE, getPath()); + + VCard protectedVCard = new VCard(); + protectedVCard.setVersion(exposedVCard.getVersion()); + protectedVCard.setUid(exposedVCard.getUid()); + + StructuredName structuredName = new StructuredName(); + structuredName.setGiven("Open"); + structuredName.addAdditional("Whisper"); + structuredName.setFamily("Systems"); + protectedVCard.setStructuredName(structuredName); + protectedVCard.setFormattedName("Open Whisper Systems"); + + if (exposedVCard.getPhotos().size() > 0) { + Photo exposedPhoto = exposedVCard.getPhotos().get(0); + byte[] protectedPhotoData = HidingUtil.encryptEncodeAndPrefix(masterCipher, exposedPhoto.getData()); + Photo protectedPhoto = new Photo(protectedPhotoData, ImageType.JPEG); + exposedVCard.removeProperties(Photo.class); + + protectedPhoto.addParameter(PARAMETER_NAME_FLOCK_HIDDEN_PHOTO, "true"); + protectedVCard.addPhoto( protectedPhoto); + } + + protectedVCard.addExtendedProperty(PROPERTY_NAME_FLOCK_HIDDEN, + HidingUtil.encryptEncodeAndPrefix(masterCipher, Ezvcard.write(exposedVCard).go())); + + super.putComponentToServer(protectedVCard, ifMatchETag); + } + + @Override + public void addHiddenComponent(VCard component) + throws InvalidComponentException, DavException, GeneralSecurityException, IOException + { + putHiddenComponentToServer(component, Optional.absent()); + fetchProperties(); + } + + @Override + public void updateHiddenComponent(ComponentETagPair component) + throws InvalidComponentException, DavException, GeneralSecurityException, IOException + { + putHiddenComponentToServer(component.getComponent(), component.getETag()); + fetchProperties(); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/HidingCardDavStore.java b/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/HidingCardDavStore.java new file mode 100644 index 0000000..0ac0fbb --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/HidingCardDavStore.java @@ -0,0 +1,184 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.addressbook; + +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.webdav.carddav.CardDavConstants; + +import org.anhonesteffort.flock.crypto.MasterCipher; +import org.anhonesteffort.flock.sync.HidingDavStore; +import org.anhonesteffort.flock.sync.HidingUtil; +import org.anhonesteffort.flock.webdav.DavClient; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.anhonesteffort.flock.webdav.carddav.CardDavCollection; +import org.anhonesteffort.flock.webdav.carddav.CardDavStore; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.client.methods.PropFindMethod; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.LinkedList; +import java.util.List; + +/** + * Programmer: rhodey + */ +public class HidingCardDavStore implements HidingDavStore { + + private MasterCipher masterCipher; + private CardDavStore cardDavStore; + + public HidingCardDavStore(MasterCipher masterCipher, + String hostHREF, + String username, + String password, + Optional currentUserPrincipal, + Optional addressBookHomeSet) + throws DavException, IOException + { + this.masterCipher = masterCipher; + this.cardDavStore = new CardDavStore(hostHREF, username, password, currentUserPrincipal, addressBookHomeSet); + } + + public HidingCardDavStore(MasterCipher masterCipher, + DavClient client, + Optional currentUserPrincipal, + Optional addressBookHomeSet) + { + this.masterCipher = masterCipher; + this.cardDavStore = new CardDavStore(client, currentUserPrincipal, addressBookHomeSet); + } + + @Override + public String getHostHREF() { + return cardDavStore.getHostHREF(); + } + + public Optional getAddressbookHomeSet() + throws PropertyParseException, DavException, IOException + { + return cardDavStore.getAddressbookHomeSet(); + } + + @Override + public Optional getCollection(String path) throws DavException, IOException { + HidingCardDavCollection targetCollection = new HidingCardDavCollection(cardDavStore, path, masterCipher); + DavPropertyNameSet collectionProps = targetCollection.getPropertyNamesForFetch(); + PropFindMethod propFindMethod = new PropFindMethod(path, collectionProps, PropFindMethod.DEPTH_0); + + try { + + cardDavStore.getClient().execute(propFindMethod); + + MultiStatus multiStatus = propFindMethod.getResponseBodyAsMultiStatus(); + MultiStatusResponse[] responses = multiStatus.getResponses(); + List returnedCollections = CardDavStore.getCollectionsFromMultiStatusResponses(cardDavStore, responses); + + if (returnedCollections.size() == 0) + Optional.absent(); + + return Optional.of(new HidingCardDavCollection(returnedCollections.get(0), masterCipher)); + + } catch (DavException e) { + + if (e.getErrorCode() == DavServletResponse.SC_NOT_FOUND) + return Optional.absent(); + + throw e; + + } finally { + propFindMethod.releaseConnection(); + } + } + + @Override + public List getCollections() + throws PropertyParseException, DavException, IOException + { + Optional addressbookHomeSetUri = getAddressbookHomeSet(); + if (!addressbookHomeSetUri.isPresent()) + throw new PropertyParseException("No addressbook-home-set property found for user.", + getHostHREF(), CardDavConstants.PROPERTY_NAME_ADDRESSBOOK_HOME_SET); + + HidingCardDavCollection hack = new HidingCardDavCollection(cardDavStore, "hack", masterCipher); + DavPropertyNameSet addressbookProps = hack.getPropertyNamesForFetch(); + + PropFindMethod method = new PropFindMethod(getHostHREF().concat(addressbookHomeSetUri.get()), + addressbookProps, + PropFindMethod.DEPTH_1); + + try { + + cardDavStore.getClient().execute(method); + + MultiStatus multiStatus = method.getResponseBodyAsMultiStatus(); + MultiStatusResponse[] responses = multiStatus.getResponses(); + + List hidingCollections = new LinkedList(); + List collections = CardDavStore.getCollectionsFromMultiStatusResponses(cardDavStore, responses); + + for (CardDavCollection collection : collections) + hidingCollections.add(new HidingCardDavCollection(collection, masterCipher)); + + return hidingCollections; + + } finally { + method.releaseConnection(); + } + } + + @Override + public void addCollection(String path) + throws DavException, IOException, GeneralSecurityException + { + cardDavStore.addCollection(path); + } + + @Override + public void removeCollection(String path) throws DavException, IOException { + cardDavStore.removeCollection(path); + } + + public void addCollection(String path, + String displayName) + throws DavException, IOException, GeneralSecurityException + { + DavPropertySet properties = new DavPropertySet(); + String hiddenDisplayName = HidingUtil.encryptEncodeAndPrefix(masterCipher, displayName); + + properties.add(new DefaultDavProperty(DavPropertyName.DISPLAYNAME, hiddenDisplayName)); + + cardDavStore.addCollection(path, properties); + } + + @Override + public void releaseConnections() { + cardDavStore.closeHttpConnection(); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/LocalAddressbookStore.java b/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/LocalAddressbookStore.java new file mode 100644 index 0000000..29a475f --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/LocalAddressbookStore.java @@ -0,0 +1,124 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.addressbook; + +import android.content.ContentProviderClient; +import android.content.Context; +import android.os.RemoteException; +import android.util.Log; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.sync.LocalComponentStore; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.LinkedList; +import java.util.List; + +/** + * Programmer: rhodey + */ +public class LocalAddressbookStore implements LocalComponentStore { + + private static final String TAG = "org.anhonesteffort.flock.sync.addressbook.LocalAddressbookStore"; + + private Context context; + private ContentProviderClient client; + private DavAccount account; + + // NOTICE: Android only allows for one "address book" per account, so without multi-account + // NOTICE... support we get one CardDAV collection and thus one address book. + public LocalAddressbookStore(Context context, + ContentProviderClient client, + DavAccount account) + { + this.context = context; + this.client = client; + this.account = account; + } + + public LocalAddressbookStore(Context context, + DavAccount account) + { + this.context = context; + this.account = account; + + client = context.getContentResolver().acquireContentProviderClient + (AddressbookSyncScheduler.CONTENT_AUTHORITY); + } + + @Override + public Optional getCollection(String remotePath) + throws RemoteException + { + Optional collectionPath = account.getCardDavCollectionPath(context); + String remotePathDecoded; + + try { + + remotePathDecoded = (URLDecoder.decode(remotePath, "UTF8")); + + } catch (UnsupportedEncodingException e) { + Log.e(TAG, "caught exception while returning collection", e); + throw new RemoteException(e.toString()); // HACK :( + } + + if (collectionPath.isPresent() && + (collectionPath.get().equals(remotePath) || collectionPath.get().equals(remotePathDecoded))) + { + return Optional.of(new LocalContactCollection(context, + client, + account.getOsAccount(), + collectionPath.get())); + } + + return Optional.absent(); + } + + @Override + public void addCollection(String remotePath) { + account.setCardDavCollection(context, remotePath); + } + + @Override + public void addCollection(String remotePath, String displayName) { + account.setCardDavCollection(context, remotePath); + + LocalContactCollection collection = new LocalContactCollection(context, client, account.getOsAccount(), remotePath); + collection.setDisplayName(displayName); + } + + @Override + public void removeCollection(String remotePath) { + account.setCardDavCollection(context, null); + } + + @Override + public List getCollections() { + List collections = new LinkedList(); + Optional collectionPath = account.getCardDavCollectionPath(context); + + if (collectionPath.isPresent()) + collections.add(new LocalContactCollection(context, client, account.getOsAccount(), collectionPath.get())); + + return collections; + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/LocalContactCollection.java b/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/LocalContactCollection.java new file mode 100644 index 0000000..0f6eb0d --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/addressbook/LocalContactCollection.java @@ -0,0 +1,682 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.addressbook; + +import android.accounts.Account; +import android.content.ContentProviderClient; +import android.content.ContentProviderOperation; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; +import android.provider.ContactsContract; +import android.util.Log; + +import com.google.common.base.Optional; +import ezvcard.VCard; + +import org.anhonesteffort.flock.webdav.carddav.CardDavConstants; +import org.anhonesteffort.flock.sync.AbstractLocalComponentCollection; +import org.anhonesteffort.flock.webdav.ComponentETagPair; +import org.anhonesteffort.flock.webdav.InvalidComponentException; + +import java.util.LinkedList; +import java.util.List; + +/** + * Programmer: rhodey + */ +public class LocalContactCollection extends AbstractLocalComponentCollection { + + private static final String TAG = "org.anhonesteffort.flock.sync.addressbook.LocalContactCollection"; + + private static final String PREFERENCES_NAME = "org.anhonesteffort.flock.sync.addressbook.LocalContactCollection"; + private static final String KEY_PREFIX_COLLECTION_C_TAG = "LocalContactCollection.KEY_PREFIX_COLLECTION_C_TAG"; + private static final String KEY_COLLECTION_DISPLAY_NAME = "LocalContactCollection.KEY_COLLECTION_DISPLAY_NAME"; + + private Context context; + + public LocalContactCollection(Context context, + ContentProviderClient client, + Account account, + String remotePath) + { + super(client, account, remotePath, 1L); // hack :D limit one collection + this.context = context; + } + + private String getKeyForCTag() { + return KEY_PREFIX_COLLECTION_C_TAG.concat(getPath()); + } + + public static Uri getSyncAdapterUri(Uri base, Account account) { + return base.buildUpon() + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, account.name) + .appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type) + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") + .build(); + } + + @Override + protected Uri getSyncAdapterUri(Uri base) { + return getSyncAdapterUri(base, account); + } + + @Override + protected Uri getUriForComponents() { + return getSyncAdapterUri(ContactsContract.RawContacts.CONTENT_URI); + } + + private Uri getUriForData() { + return getSyncAdapterUri(ContactsContract.Data.CONTENT_URI); + } + + private Uri getUriForPicture(Long rawContactId) { + Uri rawContactUri = ContentUris.withAppendedId( + ContactsContract.RawContacts.CONTENT_URI, + rawContactId + ); + return Uri.withAppendedPath(rawContactUri, + ContactsContract.RawContacts.DisplayPhoto.CONTENT_DIRECTORY); + } + + @Override + protected String getColumnNameCollectionLocalId() { + return "1"; // hack :D + } + + @Override + protected String getColumnNameComponentLocalId() { + return ContactsContract.RawContacts._ID; + } + + protected String getColumnNameComponentDataLocalId() { + return ContactsContract.Data.RAW_CONTACT_ID; + } + + @Override + protected String getColumnNameComponentUid() { + return ContactsContract.RawContacts.SOURCE_ID; + } + + @Override + protected String getColumnNameComponentETag() { + return ContactsContract.RawContacts.SYNC1; + } + + @Override + protected String getColumnNameDirty() { + return ContactsContract.RawContacts.DIRTY; + } + + @Override + protected String getColumnNameDeleted() { + return ContactsContract.RawContacts.DELETED; + } + + @Override + public Optional getDisplayName() { + SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_NAME, + Context.MODE_MULTI_PROCESS); + + return Optional.fromNullable(preferences.getString(KEY_COLLECTION_DISPLAY_NAME, null)); + } + + @Override + public void setDisplayName(String displayName) { + SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_NAME, + Context.MODE_MULTI_PROCESS); + + preferences.edit().putString(KEY_COLLECTION_DISPLAY_NAME, displayName).commit(); + } + + @Override + public Optional getCTag() { + SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_MULTI_PROCESS); + return Optional.fromNullable(preferences.getString(getKeyForCTag(), null)); + } + + @Override + public void setCTag(String cTag) { + SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_MULTI_PROCESS); + preferences.edit().putString(getKeyForCTag(), cTag).commit(); + } + + private void addStructuredNames(Long rawContactId, VCard vCard) + throws RemoteException + { + String SELECTION = getColumnNameComponentDataLocalId() + "=? " + + "AND " + ContactsContract.Data.MIMETYPE + "=?"; + String[] SELECTION_ARGS = new String[]{ + rawContactId.toString(), + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + }; + + Cursor cursor = client.query(getUriForData(), + ContactFactory.getProjectionForStructuredName(), + SELECTION, + SELECTION_ARGS, + null); + + while (cursor.moveToNext()) { + ContentValues structuredNameValues = ContactFactory.getValuesForStructuredName(cursor); + ContactFactory.addStructuredName(vCard, structuredNameValues); + } + cursor.close(); + } + + private void addPhoneNumbers(Long rawContactId, VCard vCard) + throws InvalidComponentException, RemoteException + { + String SELECTION = getColumnNameComponentDataLocalId() + "=? " + + "AND " + ContactsContract.Data.MIMETYPE + "=?"; + String[] SELECTION_ARGS = new String[]{ + rawContactId.toString(), + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + }; + + Cursor cursor = client.query(getUriForData(), + ContactFactory.getProjectionForPhoneNumber(), + SELECTION, + SELECTION_ARGS, + null); + + while (cursor.moveToNext()) { + ContentValues phoneNumberValues = ContactFactory.getValuesForPhoneNumber(cursor); + ContactFactory.addPhoneNumber(getPath(), vCard, phoneNumberValues); + } + cursor.close(); + } + + private void addEmailAddresses(Long rawContactId, VCard vCard) + throws InvalidComponentException, RemoteException + { + String SELECTION = getColumnNameComponentDataLocalId() + "=? " + + "AND " + ContactsContract.Data.MIMETYPE + "=?"; + String[] SELECTION_ARGS = new String[]{ + rawContactId.toString(), + ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE + }; + + Cursor cursor = client.query(getUriForData(), + ContactFactory.getProjectionForEmailAddress(), + SELECTION, + SELECTION_ARGS, + null); + + while (cursor.moveToNext()) { + ContentValues emailAddressValues = ContactFactory.getValuesForEmailAddress(cursor); + ContactFactory.addEmailAddress(getPath(), vCard, emailAddressValues); + } + cursor.close(); + } + + private void addPictures(Long rawContactId, VCard vCard) + throws InvalidComponentException, RemoteException + { + ContactFactory.addPicture(getPath(), client, getUriForPicture(rawContactId), vCard); + } + + private void addOrganizations(Long rawContactId, VCard vCard) + throws RemoteException + { + String SELECTION = getColumnNameComponentDataLocalId() + "=? " + + "AND " + ContactsContract.Data.MIMETYPE + "=?"; + String[] SELECTION_ARGS = new String[]{ + rawContactId.toString(), + ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE + }; + + Cursor cursor = client.query(getUriForData(), + ContactFactory.getProjectionForOrganization(), + SELECTION, + SELECTION_ARGS, + null); + + while (cursor.moveToNext()) { + ContentValues organizationValues = ContactFactory.getValuesForOrganization(cursor); + ContactFactory.addOrganizer(vCard, organizationValues); + } + cursor.close(); + } + + private void addInstantMessaging(Long rawContactId, VCard vCard) + throws InvalidComponentException, RemoteException + { + String SELECTION = getColumnNameComponentDataLocalId() + "=? " + + "AND " + ContactsContract.Data.MIMETYPE + "=?"; + String[] SELECTION_ARGS = new String[]{ + rawContactId.toString(), + ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE + }; + + Cursor cursor = client.query(getUriForData(), + ContactFactory.getProjectionForInstantMessaging(), + SELECTION, + SELECTION_ARGS, + null); + + while (cursor.moveToNext()) { + ContentValues instantMessagingValues = ContactFactory.getValuesForInstantMessaging(cursor); + ContactFactory.addInstantMessaging(getPath(), vCard, instantMessagingValues); + } + cursor.close(); + } + + private void addNickNames(Long rawContactId, VCard vCard) + throws RemoteException + { + String SELECTION = getColumnNameComponentDataLocalId() + "=? " + + "AND " + ContactsContract.Data.MIMETYPE + "=?"; + String[] SELECTION_ARGS = new String[]{ + rawContactId.toString(), + ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE + }; + + Cursor cursor = client.query(getUriForData(), + ContactFactory.getProjectionForNickName(), + SELECTION, + SELECTION_ARGS, + null); + + while (cursor.moveToNext()) { + ContentValues nickNameValues = ContactFactory.getValuesForNickName(cursor); + ContactFactory.addNickName(vCard, nickNameValues); + } + cursor.close(); + } + + private void addNotes(Long rawContactId, VCard vCard) + throws RemoteException + { + String SELECTION = getColumnNameComponentDataLocalId() + "=? " + + "AND " + ContactsContract.Data.MIMETYPE + "=?"; + String[] SELECTION_ARGS = new String[]{ + rawContactId.toString(), + ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE + }; + + Cursor cursor = client.query(getUriForData(), + ContactFactory.getProjectionForNote(), + SELECTION, + SELECTION_ARGS, + null); + + while (cursor.moveToNext()) { + ContentValues noteValues = ContactFactory.getValuesForNote(cursor); + ContactFactory.addNote(vCard, noteValues); + } + cursor.close(); + } + + private void addPostalAddresses(Long rawContactId, VCard vCard) + throws InvalidComponentException, RemoteException + { + String SELECTION = getColumnNameComponentDataLocalId() + "=? " + + "AND " + ContactsContract.Data.MIMETYPE + "=?"; + String[] SELECTION_ARGS = new String[]{ + rawContactId.toString(), + ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE + }; + + Cursor cursor = client.query(getUriForData(), + ContactFactory.getProjectionForPostalAddress(), + SELECTION, + SELECTION_ARGS, + null); + + while (cursor.moveToNext()) { + ContentValues postalAddressValues = ContactFactory.getValuesForPostalAddress(cursor); + ContactFactory.addPostalAddress(getPath(), vCard, postalAddressValues); + } + cursor.close(); + } + + private void addWebsites(Long rawContactId, VCard vCard) + throws InvalidComponentException, RemoteException + { + String SELECTION = getColumnNameComponentDataLocalId() + "=? " + + "AND " + ContactsContract.Data.MIMETYPE + "=?"; + String[] SELECTION_ARGS = new String[]{ + rawContactId.toString(), + ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE + }; + + Cursor cursor = client.query(getUriForData(), + ContactFactory.getProjectionForWebsite(), + SELECTION, + SELECTION_ARGS, + null); + + while (cursor.moveToNext()) { + ContentValues websiteValues = ContactFactory.getValuesForWebsite(cursor); + ContactFactory.addWebsite(getPath(), vCard, websiteValues); + } + cursor.close(); + } + + private void addEvents(Long rawContactId, VCard vCard) + throws InvalidComponentException, RemoteException + { + String SELECTION = getColumnNameComponentDataLocalId() + "=? " + + "AND " + ContactsContract.Data.MIMETYPE + "=?"; + String[] SELECTION_ARGS = new String[]{ + rawContactId.toString(), + ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE + }; + + Cursor cursor = client.query(getUriForData(), + ContactFactory.getProjectionForEvent(), + SELECTION, + SELECTION_ARGS, + null); + + while (cursor.moveToNext()) { + ContentValues eventValues = ContactFactory.getValuesForEvent(cursor); + ContactFactory.addEvent(getPath(), vCard, eventValues); + } + cursor.close(); + } + + private void addSipAddresses(Long rawContactId, VCard vCard) + throws RemoteException + { + String SELECTION = getColumnNameComponentDataLocalId() + "=? " + + "AND " + ContactsContract.Data.MIMETYPE + "=?"; + String[] SELECTION_ARGS = new String[]{ + rawContactId.toString(), + ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE + }; + + Cursor cursor = client.query(getUriForData(), + ContactFactory.getProjectionForSipAddress(), + SELECTION, + SELECTION_ARGS, + null); + + while (cursor.moveToNext()) { + ContentValues sipAddressValues = ContactFactory.getValuesForSipAddress(cursor); + ContactFactory.addSipAddress(vCard, sipAddressValues); + } + cursor.close(); + } + + private void buildContact(Long rawContactId, VCard vCard) + throws InvalidComponentException, RemoteException + { + addStructuredNames( rawContactId, vCard); + addPhoneNumbers( rawContactId, vCard); + addEmailAddresses( rawContactId, vCard); + addPictures( rawContactId, vCard); + addOrganizations( rawContactId, vCard); + addInstantMessaging(rawContactId, vCard); + addNickNames( rawContactId, vCard); + addNotes( rawContactId, vCard); + addPostalAddresses( rawContactId, vCard); + addWebsites( rawContactId, vCard); + addEvents( rawContactId, vCard); + addSipAddresses( rawContactId, vCard); + } + + @Override + public Optional getComponent(Long rawContactId) + throws InvalidComponentException, RemoteException + { + Cursor cursor = client.query(ContentUris.withAppendedId(getUriForComponents(), rawContactId), + ContactFactory.getProjectionForRawContact(), + null, + null, + null); + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + if (cursor.moveToNext()) { + ContentValues rawContactValues = ContactFactory.getValuesForRawContact(cursor); + ComponentETagPair vCard = ContactFactory.getVCard(rawContactValues); + + buildContact(rawContactId, vCard.getComponent()); + cursor.close(); + return Optional.of(vCard.getComponent()); + } + + cursor.close(); + return Optional.absent(); + } + + @Override + public Optional> getComponent(String uid) + throws InvalidComponentException, RemoteException + { + String SELECTION = getColumnNameComponentUid() + "=?"; + String[] SELECTION_ARGS = new String[]{uid}; + + + Cursor cursor = client.query(getUriForComponents(), + ContactFactory.getProjectionForRawContact(), + SELECTION, + SELECTION_ARGS, + null); + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + if (cursor.moveToNext()) { + ContentValues rawContactValues = ContactFactory.getValuesForRawContact(cursor); + Long rawContactId = rawContactValues.getAsLong(getColumnNameComponentLocalId()); + ComponentETagPair vCard = ContactFactory.getVCard(rawContactValues); + + buildContact(rawContactId, vCard.getComponent()); + cursor.close(); + + return Optional.of(vCard); + } + + cursor.close(); + return Optional.absent(); + } + + @Override + public List> getComponents() + throws InvalidComponentException, RemoteException + { + List> vCards = new LinkedList>(); + + + Cursor cursor = client.query(getUriForComponents(), + ContactFactory.getProjectionForRawContact(), + null, null, null); + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + while (cursor.moveToNext()) { + ContentValues rawContactValues = ContactFactory.getValuesForRawContact(cursor); + Long rawContactId = rawContactValues.getAsLong(getColumnNameComponentLocalId()); + ComponentETagPair vCard = ContactFactory.getVCard(rawContactValues); + + buildContact(rawContactId, vCard.getComponent()); + vCards.add(vCard); + } + + cursor.close(); + return vCards; + } + + @Override + public void addComponent(ComponentETagPair vCard) throws InvalidComponentException { + ContentValues rawContactValues = ContactFactory.getValuesForRawContact(vCard); + + int raw_contact_op_index = pendingOperations.size(); + + pendingOperations.add(ContentProviderOperation.newInsert(getUriForComponents()) + .withValues(rawContactValues) + .build()); + + Optional structuredName = ContactFactory.getValuesForStructuredName(vCard.getComponent()); + if (structuredName.isPresent()) { + pendingOperations.add(ContentProviderOperation.newInsert(getUriForData()) + .withValues(structuredName.get()) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, raw_contact_op_index) + .build()); + } + + List phoneNumbers = ContactFactory.getValuesForPhoneNumbers(vCard.getComponent()); + for (ContentValues phoneNumber : phoneNumbers) { + pendingOperations.add(ContentProviderOperation.newInsert(getUriForData()) + .withValues(phoneNumber) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, raw_contact_op_index) + .build()); + } + + List emailAddresses = ContactFactory.getValuesForEmailAddresses(vCard.getComponent()); + for (ContentValues emailAddress : emailAddresses) { + pendingOperations.add(ContentProviderOperation.newInsert(getUriForData()) + .withValues(emailAddress) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, raw_contact_op_index) + .build()); + } + + Optional picture = ContactFactory.getValuesForPicture(vCard.getComponent()); + if (picture.isPresent()) { + pendingOperations.add(ContentProviderOperation.newInsert(getUriForData()) + .withValues(picture.get()) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, raw_contact_op_index) + .build()); + } + + List organizations = ContactFactory.getValuesForOrganization(vCard.getComponent()); + for (ContentValues organization : organizations) { + pendingOperations.add(ContentProviderOperation.newInsert(getUriForData()) + .withValues(organization) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, raw_contact_op_index) + .build()); + } + + List instantMessaging = ContactFactory.getValuesForInstantMessaging(vCard.getComponent()); + for (ContentValues instantMessenger : instantMessaging) { + pendingOperations.add(ContentProviderOperation.newInsert(getUriForData()) + .withValues(instantMessenger) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, raw_contact_op_index) + .build()); + } + + List nickNames = ContactFactory.getValuesForNickName(vCard.getComponent()); + for (ContentValues nickName : nickNames) { + pendingOperations.add(ContentProviderOperation.newInsert(getUriForData()) + .withValues(nickName) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, raw_contact_op_index) + .build()); + } + + List notes = ContactFactory.getValuesForNote(vCard.getComponent()); + for (ContentValues note : notes) { + pendingOperations.add(ContentProviderOperation.newInsert(getUriForData()) + .withValues(note) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, raw_contact_op_index) + .build()); + } + + List postalAddresses = ContactFactory.getValuesForPostalAddresses(vCard.getComponent()); + for (ContentValues postalAddress : postalAddresses) { + pendingOperations.add(ContentProviderOperation.newInsert(getUriForData()) + .withValues(postalAddress) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, raw_contact_op_index) + .build()); + } + + List websites = ContactFactory.getValuesForWebsites(vCard.getComponent()); + for (ContentValues website : websites) { + pendingOperations.add(ContentProviderOperation.newInsert(getUriForData()) + .withValues(website) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, raw_contact_op_index) + .build()); + } + + List events = ContactFactory.getValuesForEvents(vCard.getComponent()); + for (ContentValues event : events) { + pendingOperations.add(ContentProviderOperation.newInsert(getUriForData()) + .withValues(event) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, raw_contact_op_index) + .build()); + } + + List sipAddresses = ContactFactory.getValuesForSipAddresses(vCard.getComponent()); + for (ContentValues sipAddress : sipAddresses) { + pendingOperations.add(ContentProviderOperation.newInsert(getUriForData()) + .withValues(sipAddress) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, raw_contact_op_index) + .build()); + } + } + + @Override + public void updateComponent(ComponentETagPair vCard) + throws InvalidComponentException, RemoteException + { + if (vCard.getComponent().getUid() != null) { + removeComponent(vCard.getComponent().getUid().getValue()); + addComponent(vCard); + } + else { + Log.e(TAG, "was given a vcard with missing uid"); + throw new InvalidComponentException("Cannot update a vCard without UID!", false, + CardDavConstants.CARDDAV_NAMESPACE, getPath()); + } + } + + public void copyToAccount(Account toAccount, ContactCopiedListener listener) + throws InvalidComponentException, RemoteException + { + Log.d(TAG, "copy my " + getComponentIds().size() + " contacts to " + toAccount.name); + + LocalContactCollection toCollection = new LocalContactCollection(context, client, toAccount, getPath()); + + for (Long contactId : getComponentIds()) { + try { + + Optional copyContact = getComponent(contactId); + if (copyContact.isPresent()) { + copyContact.get().setUid(null); + ComponentETagPair correctedContact = + new ComponentETagPair(copyContact.get(), Optional.absent()); + + toCollection.addComponent(correctedContact); + toCollection.commitPendingOperations(); + listener.onContactCopied(getAccount(), toAccount); + } + else + throw new InvalidComponentException("absent component for local id on copy", + false, CardDavConstants.CARDDAV_NAMESPACE, getPath()); + + } catch (InvalidComponentException e) { + listener.onContactCopyFailed(e, getAccount(), toAccount); + } catch (RemoteException e) { + listener.onContactCopyFailed(e, getAccount(), toAccount); + } catch (OperationApplicationException e) { + listener.onContactCopyFailed(e, getAccount(), toAccount); + } + } + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/CalendarCopiedListener.java b/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/CalendarCopiedListener.java new file mode 100644 index 0000000..1a95f2b --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/CalendarCopiedListener.java @@ -0,0 +1,37 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.calendar; + +import android.accounts.Account; + +/** + * Programmer: rhodey + */ +public interface CalendarCopiedListener { + + public void onCalendarCopied(Account fromAccount, Account toAccount, Long calendarId); + + public void onCalendarCopyFailed(Exception e, Account fromAccount, Account toAccount, Long calendarId); + + public void onEventCopied(Account fromAccount, Account toAccount, Long calendarId); + + public void onEventCopyFailed(Exception e, Account fromAccount, Account toAccount, Long calendarId); + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/CalendarSyncWorker.java b/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/CalendarSyncWorker.java new file mode 100644 index 0000000..8a93659 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/CalendarSyncWorker.java @@ -0,0 +1,325 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.calendar; + +import android.content.Context; +import android.content.OperationApplicationException; +import android.content.SyncResult; +import android.os.RemoteException; +import android.util.Log; + +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.webdav.caldav.CalDavConstants; + +import net.fortuna.ical4j.model.Calendar; +import net.fortuna.ical4j.model.ConstraintViolationException; +import net.fortuna.ical4j.model.Property; +import net.fortuna.ical4j.model.component.VEvent; +import net.fortuna.ical4j.util.Calendars; +import org.anhonesteffort.flock.crypto.InvalidMacException; +import org.anhonesteffort.flock.sync.AbstractDavSyncAdapter; +import org.anhonesteffort.flock.sync.AbstractDavSyncWorker; +import org.anhonesteffort.flock.webdav.InvalidComponentException; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.xml.Namespace; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +/** + * Programmer: rhodey + */ +public class CalendarSyncWorker extends AbstractDavSyncWorker { + + private static final String TAG = "org.anhonesteffort.flock.sync.calendar.CalendarSyncWorker"; + + protected CalendarSyncWorker(Context context, + LocalEventCollection localCollection, + HidingCalDavCollection remoteCollection) + { + super(context, localCollection, remoteCollection); + } + + private LocalEventCollection getLocalCollection() { + return (LocalEventCollection) localCollection; + } + + private HidingCalDavCollection getRemoteHidingCollection() { + return (HidingCalDavCollection) remoteCollection; + } + + @Override + protected Namespace getNamespace() { + return CalDavConstants.CALDAV_NAMESPACE; + } + + @Override + protected boolean componentHasUid(Calendar component) { + try { + + Calendars.getUid(component).getValue(); + return true; + + } catch (ConstraintViolationException e) { + return false; + } + } + + @Override + protected void prePushLocallyCreatedComponent(Calendar component) { + VEvent vEvent = (VEvent) component.getComponent(VEvent.VEVENT); + + if (vEvent != null) { + Property copyIdProp = vEvent.getProperty(EventFactory.PROPERTY_NAME_FLOCK_COPY_EVENT_ID); + if (copyIdProp != null) + vEvent.getProperties().remove(copyIdProp); + } + } + + @Override + protected void pushLocallyCreatedProperties(SyncResult result) { + super.pushLocallyCreatedProperties(result); + Log.d(TAG, "pushLocallyCreatedProperties()"); + + try { + + Optional localColor = getLocalCollection().getColor(); + if (localColor.isPresent()) { + Optional remoteColor = getRemoteHidingCollection().getHiddenColor(); + + if (!remoteColor.isPresent()) { + Log.d(TAG, "remote hidden color not present, setting using local"); + getRemoteHidingCollection().setHiddenColor(localColor.get()); + result.stats.numInserts++; + } + } + + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (PropertyParseException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (DavException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (InvalidMacException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (GeneralSecurityException e){ + AbstractDavSyncAdapter.handleException(context, e, result); + } + + try { + + Optional localTimeZone = getLocalCollection().getTimeZone(); + if (localTimeZone.isPresent()) { + Optional remoteTimeZone = getRemoteHidingCollection().getTimeZone(); + + if (!remoteTimeZone.isPresent()) { + Log.d(TAG, "remote time zone not present, setting using local"); + getRemoteHidingCollection().setTimeZone(localTimeZone.get()); + result.stats.numInserts++; + } + } + + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (PropertyParseException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (DavException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } + + @Override + protected void pushLocallyChangedProperties(SyncResult result) { + super.pushLocallyChangedProperties(result); + Log.d(TAG, "pushLocallyChangedProperties()"); + + if (localCTag.isPresent() && remoteCTag.isPresent() && localCTag.get().equals(remoteCTag.get())) { + try { + + Optional localColor = getLocalCollection().getColor(); + if (localColor.isPresent()) { + Optional remoteColor = getRemoteHidingCollection().getHiddenColor(); + if (remoteColor.isPresent() && !localColor.get().equals(remoteColor.get())) { + Log.d(TAG, "remote hidden color present, updating using local"); + getRemoteHidingCollection().setHiddenColor(localColor.get()); + result.stats.numUpdates++; + } + } + + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (PropertyParseException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (DavException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (InvalidMacException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (GeneralSecurityException e){ + AbstractDavSyncAdapter.handleException(context, e, result); + } + + try { + + Optional localTimeZone = getLocalCollection().getTimeZone(); + if (localTimeZone.isPresent()) { + Optional remoteTimeZone = getRemoteHidingCollection().getTimeZone(); + if (remoteTimeZone.isPresent() && !localTimeZone.get().equals(remoteTimeZone.get())) { + Log.d(TAG, "remote time zone present, updating using local"); + getRemoteHidingCollection().setTimeZone(localTimeZone.get()); + result.stats.numUpdates++; + } + } + + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (PropertyParseException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (DavException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } + } + + @Override + protected void pullRemotelyCreatedProperties(SyncResult result) { + super.pullRemotelyCreatedProperties(result); + Log.d(TAG, "pullRemotelyCreatedProperties()"); + + try { + + Optional remoteColor = getRemoteHidingCollection().getHiddenColor(); + if (remoteColor.isPresent()) { + Optional localColor = getLocalCollection().getColor(); + + if (!localColor.isPresent()) { + Log.d(TAG, "local color not present, setting using remote"); + getLocalCollection().setColor(remoteColor.get()); + localCollection.commitPendingOperations(); + result.stats.numInserts++; + } + } + + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (OperationApplicationException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (PropertyParseException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (InvalidMacException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (GeneralSecurityException e){ + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + + try { + + Optional remoteTimeZone = getRemoteHidingCollection().getTimeZone(); + if (remoteTimeZone.isPresent()) { + Optional localTimeZone = getLocalCollection().getTimeZone(); + + if (!localTimeZone.isPresent()) { + Log.d(TAG, "local time zone not present, setting using remote"); + getLocalCollection().setTimeZone(remoteTimeZone.get()); + localCollection.commitPendingOperations(); + result.stats.numInserts++; + } + } + + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (OperationApplicationException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (PropertyParseException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (InvalidComponentException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } + + @Override + protected void pullRemotelyChangedProperties(SyncResult result) { + super.pullRemotelyChangedProperties(result); + Log.d(TAG, "pullRemotelyChangedProperties()"); + + if (localCTag.isPresent() && remoteCTag.isPresent() && !localCTag.get().equals(remoteCTag.get())) { + try { + + Optional remoteColor = getRemoteHidingCollection().getHiddenColor(); + if (remoteColor.isPresent()) { + Optional localColor = getLocalCollection().getColor(); + if (localColor.isPresent() && !localColor.get().equals(remoteColor.get())) { + Log.d(TAG, "local color present, updating using remote"); + getLocalCollection().setColor(remoteColor.get()); + localCollection.commitPendingOperations(); + result.stats.numUpdates++; + } + } + + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (OperationApplicationException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (PropertyParseException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (InvalidMacException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (GeneralSecurityException e){ + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + + try { + + Optional remoteTimeZone = getRemoteHidingCollection().getTimeZone(); + if (remoteTimeZone.isPresent()) { + Optional localTimeZone = getLocalCollection().getTimeZone(); + if (localTimeZone.isPresent() && !localTimeZone.get().equals(remoteTimeZone.get())) { + Log.d(TAG, "local time zone present, updating using remote"); + getLocalCollection().setTimeZone(remoteTimeZone.get()); + localCollection.commitPendingOperations(); + result.stats.numUpdates++; + } + } + + } catch (RemoteException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (OperationApplicationException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (PropertyParseException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (InvalidComponentException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/CalendarsSyncScheduler.java b/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/CalendarsSyncScheduler.java new file mode 100644 index 0000000..a3fe7ef --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/CalendarsSyncScheduler.java @@ -0,0 +1,55 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.calendar; + +import android.content.Context; +import android.net.Uri; +import android.provider.CalendarContract; + +import org.anhonesteffort.flock.sync.AbstractSyncScheduler; + +/** + * Programmer: rhodey + */ +public class CalendarsSyncScheduler extends AbstractSyncScheduler { + + private static final String TAG = "org.anhonesteffort.flock.sync.calendar.CalendarsSyncScheduler"; + public static final String CONTENT_AUTHORITY = CalendarContract.AUTHORITY; + + public CalendarsSyncScheduler(Context context) { + super(context); + } + + @Override + protected String getTAG() { + return TAG; + } + + @Override + public String getAuthority() { + return CONTENT_AUTHORITY; + } + + @Override + protected Uri getUri() { + return CalendarContract.Calendars.CONTENT_URI; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/CalendarsSyncService.java b/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/CalendarsSyncService.java new file mode 100644 index 0000000..20c7559 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/CalendarsSyncService.java @@ -0,0 +1,188 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.calendar; + +import android.accounts.Account; +import android.app.Service; +import android.content.ContentProviderClient; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SyncResult; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.preference.PreferenceManager; +import android.util.Log; + +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.webdav.caldav.CalDavConstants; + +import org.anhonesteffort.flock.DavAccountHelper; +import org.anhonesteffort.flock.PreferencesActivity; +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.crypto.KeyHelper; +import org.anhonesteffort.flock.crypto.MasterCipher; +import org.anhonesteffort.flock.sync.AbstractDavSyncAdapter; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.jackrabbit.webdav.DavException; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +/** + * Programmer: rhodey + */ +public class CalendarsSyncService extends Service { + + private static final String TAG = "org.anhonesteffort.flock.sync.calendar.CalendarsSyncService"; + + private static CalendarsSyncAdapter sSyncAdapter = null; + private static final Object sSyncAdapterLock = new Object(); + + @Override + public void onCreate() { + synchronized (sSyncAdapterLock) { + if (sSyncAdapter == null) + sSyncAdapter = new CalendarsSyncAdapter(getApplicationContext()); + } + } + + @Override + public IBinder onBind(Intent intent) { + return sSyncAdapter.getSyncAdapterBinder(); + } + + private static class CalendarsSyncAdapter extends AbstractDavSyncAdapter { + + public CalendarsSyncAdapter(Context context) { + super(context); + } + + protected String getAuthority() { + return CalendarsSyncScheduler.CONTENT_AUTHORITY; + } + + private void finalizeCopiedCalendars(LocalCalendarStore localStore, + HidingCalDavStore remoteStore) + throws RemoteException, IOException, + GeneralSecurityException, DavException, PropertyParseException + { + Log.d(TAG, "finalizeCopiedCalendars()"); + + Optional calendarHome = remoteStore.getCalendarHomeSet(); + if (!calendarHome.isPresent()) + throw new PropertyParseException("No calendar-home-set property found for user.", + remoteStore.getHostHREF(), CalDavConstants.PROPERTY_NAME_CALENDAR_HOME_SET); + + List copiedCalendars = localStore.getCopiedCollections(); + for (LocalEventCollection copiedCalendar : copiedCalendars) { + String remotePath = calendarHome.get().concat(UUID.randomUUID().toString() + "/"); + Optional displayName = copiedCalendar.getDisplayName(); + Optional color = copiedCalendar.getColor(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getContext()); + + Log.d(TAG, "found copied calendar >> " + copiedCalendar.getLocalId()); + Log.d(TAG, "will put to >> " + remotePath); + + if (displayName.isPresent()) { + if (color.isPresent()) + remoteStore.addCollection(remotePath, displayName.get(), color.get()); + else { + int defaultColor = settings.getInt(PreferencesActivity.KEY_PREF_DEFAULT_CALENDAR_COLOR, 0xFFFFFFFF); + remoteStore.addCollection(remotePath, displayName.get(), defaultColor); + } + } + else + remoteStore.addCollection(remotePath); + + localStore.setCollectionPath(copiedCalendar.getLocalId(), remotePath); + localStore.setCollectionCopied(copiedCalendar.getLocalId(), false); + } + } + + @Override + public void onPerformSync(Account account, + Bundle extras, + String authority, + ContentProviderClient provider, + SyncResult syncResult) + { + Log.d(TAG, "performing sync for authority >> " + authority); + + // ical4j TimeZoneRegistry kills everything without this... + Thread.currentThread().setContextClassLoader(getContext().getClassLoader()); + + Optional davAccountOptional = DavAccountHelper.getAccount(getContext()); + if (!davAccountOptional.isPresent()) { + Log.d(TAG, "dav account is missing"); + syncResult.stats.numAuthExceptions++; + showNotifications(syncResult); + return ; + } + + try { + + Optional masterCipher = KeyHelper.getMasterCipher(getContext()); + if (!masterCipher.isPresent()) { + Log.d(TAG, "master cipher is missing"); + syncResult.stats.numAuthExceptions++; + return ; + } + + LocalCalendarStore localStore = new LocalCalendarStore(provider, davAccountOptional.get().getOsAccount()); + HidingCalDavStore remoteStore = DavAccountHelper.getHidingCalDavStore(getContext(), davAccountOptional.get(), masterCipher.get()); + + for (LocalEventCollection localCollection : localStore.getCollections()) { + Log.d(TAG, "found local collection: " + localCollection.getPath()); + Optional remoteCollection = remoteStore.getCollection(localCollection.getPath()); + + if (remoteCollection.isPresent()) + new CalendarSyncWorker(getContext(), localCollection, remoteCollection.get()).run(syncResult, false); + else { + Log.d(TAG, "local collection missing remotely, deleting locally"); + localStore.removeCollection(localCollection.getPath()); + } + } + + finalizeCopiedCalendars(localStore, remoteStore); + remoteStore.releaseConnections(); + + } catch (IOException e) { + handleException(getContext(), e, syncResult); + } catch (DavException e) { + handleException(getContext(), e, syncResult); + } catch (PropertyParseException e) { + handleException(getContext(), e, syncResult); + } catch (RemoteException e) { + handleException(getContext(), e, syncResult); + } catch (GeneralSecurityException e) { + handleException(getContext(), e, syncResult); + } + + showNotifications(syncResult); + new CalendarsSyncScheduler(getContext()).setTimeLastSync(new Date().getTime()); + } + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/EventFactory.java b/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/EventFactory.java new file mode 100644 index 0000000..4869935 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/EventFactory.java @@ -0,0 +1,942 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.calendar; + +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; +import android.provider.CalendarContract; +import android.text.TextUtils; +import android.util.Log; + +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.webdav.caldav.CalDavConstants; + +import net.fortuna.ical4j.model.Calendar; +import net.fortuna.ical4j.model.ComponentList; +import net.fortuna.ical4j.model.Date; +import net.fortuna.ical4j.model.DateTime; +import net.fortuna.ical4j.model.Dur; +import net.fortuna.ical4j.model.Parameter; +import net.fortuna.ical4j.model.ParameterList; +import net.fortuna.ical4j.model.Property; +import net.fortuna.ical4j.model.PropertyList; +import net.fortuna.ical4j.model.TimeZone; +import net.fortuna.ical4j.model.TimeZoneRegistry; +import net.fortuna.ical4j.model.TimeZoneRegistryFactory; +import net.fortuna.ical4j.model.component.VAlarm; +import net.fortuna.ical4j.model.component.VEvent; +import net.fortuna.ical4j.model.component.VToDo; +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.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.Location; +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 net.fortuna.ical4j.model.property.Summary; +import net.fortuna.ical4j.model.property.Transp; +import net.fortuna.ical4j.model.property.Trigger; +import net.fortuna.ical4j.model.property.Uid; +import net.fortuna.ical4j.model.property.Version; +import net.fortuna.ical4j.model.property.XProperty; +import org.anhonesteffort.flock.webdav.ComponentETagPair; +import org.anhonesteffort.flock.webdav.InvalidComponentException; +import org.apache.commons.lang.StringUtils; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.text.ParseException; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; + +/** + * Programmer: rhodey + * + * Much thanks to the DAVDroid project and especially + * Richard Hirner (bitfire web engineering) for leading + * the way in shoving VEvent objects into Androids Calendar + * Content Provider. This would have been much more of a + * pain without a couple hints from the DAVDroid codebase. + */ +public class EventFactory { + + private static final String TAG = "org.anhonesteffort.flock.sync.calendar.EventFactory"; + + private static final String PROPERTY_NAME_FLOCK_ORIGINAL_SYNC_ID = "X-FLOCK-ORIGINAL-SYNC-ID"; + private static final String PROPERTY_NAME_FLOCK_ORIGINAL_INSTANCE_TIME = "X-FLOCK-ORIGINAL-INSTANCE-TIME"; + protected static final String PROPERTY_NAME_FLOCK_COPY_EVENT_ID = "X-FLOCK-COPY-EVENT-ID"; + + protected static String[] getProjectionForEvent() { + return new String[] { + CalendarContract.Events._ID, // 00 + CalendarContract.Events.CALENDAR_ID, // 01 + CalendarContract.Events.ORGANIZER, // 02 + CalendarContract.Events.TITLE, // 03 + CalendarContract.Events.EVENT_LOCATION, // 04 + CalendarContract.Events.DESCRIPTION, // 05 + CalendarContract.Events.DTSTART, // 06 + CalendarContract.Events.DTEND, // 07 + CalendarContract.Events.EVENT_TIMEZONE, // 08 + CalendarContract.Events.EVENT_END_TIMEZONE, // 09 + CalendarContract.Events.DURATION, // 10 + CalendarContract.Events.ALL_DAY, // 11 + CalendarContract.Events.RRULE, // 12 + CalendarContract.Events.RDATE, // 13 + CalendarContract.Events.EXRULE, // 14 + CalendarContract.Events.EXDATE, // 15 + CalendarContract.Events.HAS_ALARM, // 16 + CalendarContract.Events.HAS_ATTENDEE_DATA, // 17 + CalendarContract.Events.ORIGINAL_ID, // 18 + CalendarContract.Events.ORIGINAL_SYNC_ID, // 19 + CalendarContract.Events.ORIGINAL_INSTANCE_TIME, // 20 + CalendarContract.Events.ORIGINAL_ALL_DAY, // 21 + CalendarContract.Events.AVAILABILITY, // 22 + CalendarContract.Events.STATUS, // 23 + CalendarContract.Events._SYNC_ID, // 24 UID + CalendarContract.Events.SYNC_DATA1, // 25 ETag + CalendarContract.Events.SYNC_DATA2 // 26 copies event id + }; + } + + protected static ContentValues getValuesForEvent(Cursor cursor) { + ContentValues values = new ContentValues(25); + + values.put(CalendarContract.Events._ID, cursor.getInt(0)); + values.put(CalendarContract.Events.CALENDAR_ID, cursor.getInt(1)); + values.put(CalendarContract.Events.ORGANIZER, cursor.getString(2)); + values.put(CalendarContract.Events.TITLE, cursor.getString(3)); + values.put(CalendarContract.Events.EVENT_LOCATION, cursor.getString(4)); + values.put(CalendarContract.Events.DESCRIPTION, cursor.getString(5)); + values.put(CalendarContract.Events.DTSTART, cursor.getLong(6)); + values.put(CalendarContract.Events.DTEND, cursor.getLong(7)); + values.put(CalendarContract.Events.EVENT_TIMEZONE, cursor.getString(8)); + values.put(CalendarContract.Events.EVENT_END_TIMEZONE, cursor.getString(9)); + values.put(CalendarContract.Events.DURATION, cursor.getString(10)); + values.put(CalendarContract.Events.ALL_DAY, (cursor.getInt(11) != 0)); + values.put(CalendarContract.Events.RRULE, cursor.getString(12)); + values.put(CalendarContract.Events.RDATE, cursor.getString(13)); + values.put(CalendarContract.Events.EXRULE, cursor.getString(14)); + values.put(CalendarContract.Events.EXDATE, cursor.getString(15)); + values.put(CalendarContract.Events.HAS_ALARM, (cursor.getInt(16) != 0)); + values.put(CalendarContract.Events.HAS_ATTENDEE_DATA, (cursor.getInt(17) != 0)); + values.put(CalendarContract.Events.ORIGINAL_ID, cursor.getLong(18)); + values.put(CalendarContract.Events.ORIGINAL_SYNC_ID, cursor.getString(19)); + values.put(CalendarContract.Events.ORIGINAL_INSTANCE_TIME, cursor.getLong(20)); + values.put(CalendarContract.Events.ORIGINAL_ALL_DAY, (cursor.getInt(21) != 0)); + values.put(CalendarContract.Events.AVAILABILITY, cursor.getInt(22)); + values.put(CalendarContract.Events.STATUS, cursor.getInt(23)); + values.put(CalendarContract.Events._SYNC_ID, cursor.getString(24)); + values.put(CalendarContract.Events.SYNC_DATA1, cursor.getString(25)); + values.put(CalendarContract.Events.SYNC_DATA2, cursor.getLong(26)); + + return values; + } + + protected static boolean isRecurrenceException(VEvent vEvent) { + return vEvent.getProperty(EventFactory.PROPERTY_NAME_FLOCK_ORIGINAL_SYNC_ID) != null; + } + + protected static boolean isCopiedRecurrenceWithExceptions(VEvent vEvent) { + return vEvent.getProperty(EventFactory.PROPERTY_NAME_FLOCK_COPY_EVENT_ID) != null; + } + + protected static Optional getLocalIdFromCopiedRecurrenceWithExceptions(VEvent event) { + if (!isCopiedRecurrenceWithExceptions(event)) + return Optional.absent(); + + return Optional.of(Long.valueOf( + event.getProperty(EventFactory.PROPERTY_NAME_FLOCK_COPY_EVENT_ID).getValue() + )); + } + + protected static void handleAttachPropertiesForCopiedRecurrenceWithExceptions(VEvent event, + Long eventId) + { + event.getProperties().add(new XProperty(PROPERTY_NAME_FLOCK_COPY_EVENT_ID, String.valueOf(eventId))); + + String uid = UUID.randomUUID().toString(); + Log.d(TAG, "setting uuid as " + uid); + + if (event.getUid() != null) + event.getUid().setValue(uid); + else + event.getProperties().add(new Uid(uid)); + } + + private static void handleAttachPropertiesForCopiedRecurrenceWithExceptions(ContentValues values, + VEvent event) + { + Long copiedEventId = values.getAsLong(CalendarContract.Events.SYNC_DATA2); + + if (copiedEventId != null && copiedEventId > 0) + event.getProperties().add(new XProperty(PROPERTY_NAME_FLOCK_COPY_EVENT_ID, + String.valueOf(copiedEventId))); + } + + private static void handleAddValuesForRecurrenceProperties(VEvent event, + ContentValues values) + { + Property copyIdProp = event.getProperty(PROPERTY_NAME_FLOCK_COPY_EVENT_ID); + + if (copyIdProp != null) + values.put(CalendarContract.Events.SYNC_DATA2, Long.valueOf(copyIdProp.getValue())); + } + + protected static void handleReplaceOriginalSyncId(String path, String syncId, VEvent event) + throws InvalidComponentException + { + try { + + Property originalSyncIdProp = event.getProperty(PROPERTY_NAME_FLOCK_ORIGINAL_SYNC_ID); + if (originalSyncIdProp != null) + originalSyncIdProp.setValue(syncId); + else + event.getProperties().add(new XProperty(PROPERTY_NAME_FLOCK_ORIGINAL_SYNC_ID, syncId)); + + } catch (ParseException e) { + throw new InvalidComponentException("cmon now ical4j", false, CalDavConstants.CALDAV_NAMESPACE, path, e); + } catch (URISyntaxException e) { + throw new InvalidComponentException("cmon now ical4j", false, CalDavConstants.CALDAV_NAMESPACE, path, e); + } catch (IOException e) { + throw new InvalidComponentException("cmon now ical4j", false, CalDavConstants.CALDAV_NAMESPACE, path, e); + } + } + + private static void handleAddValuesForDeletionExceptionToRecurring(LocalEventCollection hack, + VEvent vEvent, + ContentValues eventValues) + throws InvalidComponentException, RemoteException + { + Log.w(TAG, "gonna try and import deletion exception to androids recurrence model..."); + + Property originalSyncIdProp = vEvent.getProperty(PROPERTY_NAME_FLOCK_ORIGINAL_SYNC_ID); + + if (originalSyncIdProp == null || TextUtils.isEmpty(originalSyncIdProp.getValue())) + throw new InvalidComponentException("original sync id prop required on recurring event deletion exceptions", + false, CalDavConstants.CALDAV_NAMESPACE, hack.getPath()); + + if (vEvent.getStartDate() == null || vEvent.getStartDate().getDate() == null) + throw new InvalidComponentException("deletion exception VEvents must have a start time", + false, CalDavConstants.CALDAV_NAMESPACE, hack.getPath()); + + eventValues.put(CalendarContract.Events.ORIGINAL_INSTANCE_TIME, + vEvent.getStartDate().getDate().getTime()); + + if (vEvent.getProperty(RRule.RRULE) == null && + vEvent.getProperty(RRule.RDATE) == null && + (vEvent.getEndDate() == null || vEvent.getEndDate().getDate() == null) && + vEvent.getDuration() == null) + { + eventValues.put(CalendarContract.Events.ALL_DAY, 1); + eventValues.put(CalendarContract.Events.ORIGINAL_ALL_DAY, 1); + } + else if ((vEvent.getProperty(RRule.RRULE) != null || + vEvent.getProperty(RRule.RDATE) != null) && + vEvent.getDuration() == null) + { + eventValues.put(CalendarContract.Events.ALL_DAY, 1); + eventValues.put(CalendarContract.Events.ORIGINAL_ALL_DAY, 1); + } + + Optional originalLocalId = hack.getLocalIdForUid(originalSyncIdProp.getValue()); + if (!originalLocalId.isPresent()) { + throw new InvalidComponentException("unable to build content values for recurrence deletion " + + "exception, cannot find original event " + + originalSyncIdProp.getValue() + " in collection", + false, CalDavConstants.CALDAV_NAMESPACE, hack.getPath()); + } + + eventValues.put(CalendarContract.Events.ORIGINAL_ID, originalLocalId.get()); + eventValues.put(CalendarContract.Events.ORIGINAL_SYNC_ID, originalSyncIdProp.getValue()); + eventValues.put(CalendarContract.Events.STATUS, CalendarContract.Events.STATUS_CANCELED); + } + + private static void handleAddValuesForEditExceptionToRecurring(LocalEventCollection hack, + VEvent vEvent, + ContentValues eventValues) + throws InvalidComponentException, RemoteException + { + Log.w(TAG, "gonna try and import edit exception to androids recurrence model..."); + + Property originalSyncIdProp = vEvent.getProperty(PROPERTY_NAME_FLOCK_ORIGINAL_SYNC_ID); + Property originalInstanceTime = vEvent.getProperty(PROPERTY_NAME_FLOCK_ORIGINAL_INSTANCE_TIME); + + if (originalSyncIdProp == null || TextUtils.isEmpty(originalSyncIdProp.getValue())) + throw new InvalidComponentException("original sync id prop required on recurring event edit exceptions", + false, CalDavConstants.CALDAV_NAMESPACE, hack.getPath()); + + if (originalInstanceTime == null || TextUtils.isEmpty(originalInstanceTime.getValue())) + throw new InvalidComponentException("original instance time prop required on recurring event edit exceptions", + false, CalDavConstants.CALDAV_NAMESPACE, hack.getPath()); + + + eventValues.put(CalendarContract.Events.ORIGINAL_INSTANCE_TIME, + Long.valueOf(originalInstanceTime.getValue())); + + if (vEvent.getProperty(RRule.RRULE) == null && + vEvent.getProperty(RRule.RDATE) == null && + (vEvent.getEndDate() == null || vEvent.getEndDate().getDate() == null) && + vEvent.getDuration() == null) + { + eventValues.put(CalendarContract.Events.ALL_DAY, 1); + eventValues.put(CalendarContract.Events.ORIGINAL_ALL_DAY, 1); + } + else if ((vEvent.getProperty(RRule.RRULE) != null || + vEvent.getProperty(RRule.RDATE) != null) && + vEvent.getDuration() == null) + { + eventValues.put(CalendarContract.Events.ALL_DAY, 1); + eventValues.put(CalendarContract.Events.ORIGINAL_ALL_DAY, 1); + } + + Optional originalLocalId = hack.getLocalIdForUid(originalSyncIdProp.getValue()); + if (!originalLocalId.isPresent()) { + throw new InvalidComponentException("unable to build content values for recurrence edit " + + "exception, cannot find original event " + + originalSyncIdProp.getValue() + " in collection", + false, CalDavConstants.CALDAV_NAMESPACE, hack.getPath()); + } + + eventValues.put(CalendarContract.Events.ORIGINAL_ID, originalLocalId.get()); + eventValues.put(CalendarContract.Events.ORIGINAL_SYNC_ID, originalSyncIdProp.getValue()); + } + + protected static ContentValues getValuesForEvent(LocalEventCollection hack, + Long calendarId, + ComponentETagPair component) + throws InvalidComponentException, RemoteException + { + VEvent vEvent = (VEvent) component.getComponent().getComponent(VEvent.VEVENT); + + if (vEvent != null) { + ContentValues values = new ContentValues(); + + handleAddValuesForRecurrenceProperties(vEvent, values); + values.put(CalendarContract.Events.CALENDAR_ID, calendarId); + + if (vEvent.getUid() != null && vEvent.getUid().getValue() != null) + values.put(CalendarContract.Events._SYNC_ID, vEvent.getUid().getValue()); + else + values.putNull(CalendarContract.Events._SYNC_ID); + + if (component.getETag().isPresent()) + values.put(CalendarContract.Events.SYNC_DATA1, component.getETag().get()); + + DtStart dtStart = vEvent.getStartDate(); + if (dtStart != null && dtStart.getDate() != null) { + if (dtStart.getTimeZone() != null) + values.put(CalendarContract.Events.EVENT_TIMEZONE, dtStart.getTimeZone().getID()); + + values.put(CalendarContract.Events.DTSTART, dtStart.getDate().getTime()); + } + else { + Log.e(TAG, "no start date found on event"); + throw new InvalidComponentException("no start date found on event", false, + CalDavConstants.CALDAV_NAMESPACE, hack.getPath()); + } + + Status status = vEvent.getStatus(); + Property originalInstanceTimeProp = vEvent.getProperty(PROPERTY_NAME_FLOCK_ORIGINAL_INSTANCE_TIME); + + if (status != null && status != Status.VEVENT_CANCELLED && originalInstanceTimeProp != null) + handleAddValuesForEditExceptionToRecurring(hack, vEvent, values); + + if (status != null && status == Status.VEVENT_CONFIRMED) + values.put(CalendarContract.Events.STATUS, CalendarContract.Events.STATUS_CONFIRMED); + else if (status != null && status == Status.VEVENT_CANCELLED) + handleAddValuesForDeletionExceptionToRecurring(hack, vEvent, values); + else + values.put(CalendarContract.Events.STATUS, CalendarContract.Events.STATUS_TENTATIVE); + + Summary summary = vEvent.getSummary(); + if (summary != null) + values.put(CalendarContract.Events.TITLE, summary.getValue()); + + Location location = vEvent.getLocation(); + if (location != null) + values.put(CalendarContract.Events.EVENT_LOCATION, location.getValue()); + + Description description = vEvent.getDescription(); + if (description != null) + values.put(CalendarContract.Events.DESCRIPTION, description.getValue()); + + Transp transparency = vEvent.getTransparency(); + if (transparency != null && transparency == Transp.OPAQUE) + values.put(CalendarContract.Events.AVAILABILITY, CalendarContract.Events.AVAILABILITY_BUSY); + else + values.put(CalendarContract.Events.AVAILABILITY, CalendarContract.Events.AVAILABILITY_FREE); + + Organizer organizer = vEvent.getOrganizer(); + if (organizer != null && organizer.getCalAddress() != null) { + URI organizerAddress = organizer.getCalAddress(); + if (organizerAddress.getScheme() != null && organizerAddress.getScheme().equalsIgnoreCase("mailto")) + values.put(CalendarContract.Events.ORGANIZER, organizerAddress.getSchemeSpecificPart()); + } + + RRule rRule = (RRule) vEvent.getProperty(RRule.RRULE); + RDate rDate = (RDate) vEvent.getProperty(RDate.RDATE); + ExRule exRule = (ExRule) vEvent.getProperty(ExRule.EXRULE); + ExDate exDate = (ExDate) vEvent.getProperty(ExDate.EXDATE); + + if (rRule != null) + values.put(CalendarContract.Events.RRULE, rRule.getValue()); + if (rDate != null) + values.put(CalendarContract.Events.RDATE, rDate.getValue()); + if (exRule != null) + values.put(CalendarContract.Events.EXRULE, exRule.getValue()); + if (exDate != null) + values.put(CalendarContract.Events.EXDATE, exDate.getValue()); + + if (rRule == null && rDate == null) { + + DtEnd dtEnd = vEvent.getEndDate(); + if (dtEnd != null && dtEnd.getDate() != null) { + if (dtEnd.getTimeZone() != null) + values.put(CalendarContract.Events.EVENT_TIMEZONE, dtEnd.getTimeZone().getID()); + + values.put(CalendarContract.Events.DTEND, dtEnd.getDate().getTime()); + } + else if (vEvent.getDuration() != null) { + Duration duration = vEvent.getDuration(); + java.util.Date endDate = duration.getDuration().getTime(dtStart.getDate()); + values.put(CalendarContract.Events.DTEND, endDate.getTime()); + } + else + values.put(CalendarContract.Events.ALL_DAY, 1); + + } else if (vEvent.getDuration() != null) + values.put(CalendarContract.Events.DURATION, vEvent.getDuration().getValue()); + else + values.put(CalendarContract.Events.ALL_DAY, 1); + + PropertyList attendees = vEvent.getProperties(Attendee.ATTENDEE); + if (attendees != null && attendees.size() > 0) + values.put(CalendarContract.Events.HAS_ATTENDEE_DATA, 1); + else + values.put(CalendarContract.Events.HAS_ATTENDEE_DATA, 0); + + PropertyList alarms = vEvent.getProperties(VAlarm.VALARM); + if (alarms != null && alarms.size() > 0) + values.put(CalendarContract.Events.HAS_ALARM, 1); + else + values.put(CalendarContract.Events.HAS_ALARM, 0); + + return values; + } + + Log.e(TAG, "no VEVENT found in component"); + throw new InvalidComponentException("no VEVENT found in component", false, + CalDavConstants.CALDAV_NAMESPACE, hack.getPath()); + } + + private static void handleAddPropertiesForDeletionExceptionToRecurring(String path, + ContentValues eventValues, + VEvent vEvent) + throws InvalidComponentException + { + Log.w(TAG, "gonna try and export deletion exception from androids recurrence model..."); + vEvent.getProperties().add(Status.VEVENT_CANCELLED); + + Long originalLocalId = eventValues.getAsLong(CalendarContract.Events.ORIGINAL_ID); + String originalSyncId = eventValues.getAsString(CalendarContract.Events.ORIGINAL_SYNC_ID); + String syncId = eventValues.getAsString(CalendarContract.Events._SYNC_ID); + + if (TextUtils.isEmpty(originalSyncId)) + throw new InvalidComponentException("original sync id required on recurring event deletion exceptions", + false, CalDavConstants.CALDAV_NAMESPACE, path); + + if (originalLocalId == null || originalLocalId < 1) + throw new InvalidComponentException("original local id required on recurring event deletion exceptions", + false, CalDavConstants.CALDAV_NAMESPACE, path); + + if (vEvent.getUid() == null) + vEvent.getProperties().add(new Uid(syncId)); + else if (vEvent.getUid().getValue() == null) + vEvent.getUid().setValue(syncId); + + XProperty originalSyncIdProp = new XProperty(PROPERTY_NAME_FLOCK_ORIGINAL_SYNC_ID, originalSyncId); + vEvent.getProperties().add(originalSyncIdProp); + } + + private static void handleAddPropertiesForEditExceptionToRecurring(String path, + ContentValues eventValues, + VEvent vEvent) + throws InvalidComponentException + { + Log.w(TAG, "gonna try and export edit exception from androids recurrence model..."); + + Long originalLocalId = eventValues.getAsLong(CalendarContract.Events.ORIGINAL_ID); + String originalSyncId = eventValues.getAsString(CalendarContract.Events.ORIGINAL_SYNC_ID); + Long originalInstanceTime = eventValues.getAsLong(CalendarContract.Events.ORIGINAL_INSTANCE_TIME); + String syncId = eventValues.getAsString(CalendarContract.Events._SYNC_ID); + + if (TextUtils.isEmpty(originalSyncId)) + throw new InvalidComponentException("original sync id required on recurring event edit exceptions", + false, CalDavConstants.CALDAV_NAMESPACE, path); + + if (originalLocalId == null || originalLocalId < 1 || originalInstanceTime == null) + throw new InvalidComponentException("original local id and instance time required on recurring event edit exceptions", + false, CalDavConstants.CALDAV_NAMESPACE, path); + + if (vEvent.getUid() == null) + vEvent.getProperties().add(new Uid(syncId)); + else if (vEvent.getUid().getValue() == null) + vEvent.getUid().setValue(syncId); + + vEvent.getProperties().add(new XProperty(PROPERTY_NAME_FLOCK_ORIGINAL_SYNC_ID, originalSyncId)); + vEvent.getProperties().add(new XProperty(PROPERTY_NAME_FLOCK_ORIGINAL_INSTANCE_TIME, String.valueOf(originalInstanceTime))); + } + + protected static ComponentETagPair getEventComponent(String path, + ContentValues eventValues) + throws InvalidComponentException + { + Calendar calendar = new Calendar(); + VEvent vEvent = new VEvent(); + TimeZoneRegistry registry = TimeZoneRegistryFactory.getInstance().createRegistry(); + + calendar.getProperties().add(Version.VERSION_2_0); + handleAttachPropertiesForCopiedRecurrenceWithExceptions(eventValues, vEvent); + + String uidText = eventValues.getAsString(CalendarContract.Events._SYNC_ID); + if (!StringUtils.isEmpty(uidText)) { + Uid eventUid = new Uid(uidText); + vEvent.getProperties().add(eventUid); + } + + try { + + String organizerText = eventValues.getAsString(CalendarContract.Events.ORGANIZER); + if (StringUtils.isNotEmpty(organizerText)) { + URI organizerEmail = new URI("mailto", organizerText, null); + Organizer organizer = new Organizer(organizerEmail); + vEvent.getProperties().add(organizer); + } + + } catch (URISyntaxException e) { + Log.e(TAG, "caught exception while parsing URI from organizerText", e); + throw new InvalidComponentException("caught exception while parsing URI from organizerText", + false, CalDavConstants.CALDAV_NAMESPACE, path, e); + } + + String summaryText = eventValues.getAsString(CalendarContract.Events.TITLE); + if (StringUtils.isNotEmpty(summaryText)) { + Summary summary = new Summary(summaryText); + vEvent.getProperties().add(summary); + } + + String locationText = eventValues.getAsString(CalendarContract.Events.EVENT_LOCATION); + if (StringUtils.isNotEmpty(locationText)) { + Location location = new Location(locationText); + vEvent.getProperties().add(location); + } + + String descriptionText = eventValues.getAsString(CalendarContract.Events.DESCRIPTION); + if (StringUtils.isNotEmpty(descriptionText)) { + Description description = new Description(descriptionText); + vEvent.getProperties().add(description); + } + + Integer status = eventValues.getAsInteger(CalendarContract.Events.STATUS); + Long originalInstanceTime = eventValues.getAsLong(CalendarContract.Events.ORIGINAL_INSTANCE_TIME); + if (status != null && status != CalendarContract.Events.STATUS_CANCELED && + originalInstanceTime != null && originalInstanceTime > 0) + { + handleAddPropertiesForEditExceptionToRecurring(path, eventValues, vEvent); + } + + if (status != null && status == CalendarContract.Events.STATUS_CONFIRMED) + vEvent.getProperties().add(Status.VEVENT_CONFIRMED); + else if (status != null && status == CalendarContract.Events.STATUS_CANCELED) + handleAddPropertiesForDeletionExceptionToRecurring(path, eventValues, vEvent); + else + vEvent.getProperties().add(Status.VEVENT_TENTATIVE); + + Integer availability = eventValues.getAsInteger(CalendarContract.Events.AVAILABILITY); + if (availability != null && availability == CalendarContract.Events.AVAILABILITY_BUSY) + vEvent.getProperties().add(Transp.OPAQUE); + else + vEvent.getProperties().add(Transp.TRANSPARENT); + + Long dtStartMilliseconds = eventValues.getAsLong(CalendarContract.Events.DTSTART); + if (dtStartMilliseconds == null) + dtStartMilliseconds = eventValues.getAsLong(CalendarContract.Events.ORIGINAL_INSTANCE_TIME); + + if (dtStartMilliseconds != null) { + DtStart dtStart = new DtStart(new Date(dtStartMilliseconds)); + String dtStartTZText = eventValues.getAsString(CalendarContract.Events.EVENT_TIMEZONE); + + if (dtStartTZText != null) { + DateTime startDate = new DateTime(dtStartMilliseconds); + TimeZone startTimeZone = registry.getTimeZone(dtStartTZText); + startDate.setTimeZone(startTimeZone); + + dtStart = new DtStart(startDate); + } + + vEvent.getProperties().add(dtStart); + } + else { + Log.e(TAG, "no start date found on event"); + throw new InvalidComponentException("no start date found on event", false, + CalDavConstants.CALDAV_NAMESPACE, path); + } + + Long dtEndMilliseconds = eventValues.getAsLong(CalendarContract.Events.DTEND); + if (dtEndMilliseconds != null && dtEndMilliseconds > 0) { + DtEnd dtEnd = new DtEnd(new Date(dtEndMilliseconds)); + String dtStartTZText = eventValues.getAsString(CalendarContract.Events.EVENT_TIMEZONE); + + if (dtStartTZText != null) { + DateTime endDate = new DateTime(dtEndMilliseconds); + TimeZone endTimeZone = registry.getTimeZone(dtStartTZText); + endDate.setTimeZone(endTimeZone); + + dtEnd = new DtEnd(endDate); + } + + vEvent.getProperties().add(dtEnd); + } + + String durationText = eventValues.getAsString(CalendarContract.Events.DURATION); + if (StringUtils.isNotEmpty(durationText)) { + Dur dur = new Dur(durationText); + Duration duration = new Duration(dur); + vEvent.getProperties().add(duration); + } + + try { + + String rRuleText = eventValues.getAsString(CalendarContract.Events.RRULE); + if (StringUtils.isNotEmpty(rRuleText)) { + RRule rRule = new RRule(rRuleText); + vEvent.getProperties().add(rRule); + } + + String rDateText = eventValues.getAsString(CalendarContract.Events.RDATE); + if (StringUtils.isNotEmpty(rDateText)) { + RDate rDate = new RDate(); + rDate.setValue(rDateText); + vEvent.getProperties().add(rDate); + } + + String exRuleText = eventValues.getAsString(CalendarContract.Events.EXRULE); + if (StringUtils.isNotEmpty(exRuleText)) { + ExRule exRule = new ExRule(); + exRule.setValue(exRuleText); + vEvent.getProperties().add(exRule); + } + + String exDateText = eventValues.getAsString(CalendarContract.Events.EXDATE); + if (StringUtils.isNotEmpty(exDateText)) { + ExDate exDate = new ExDate(); + exDate.setValue(exDateText); + vEvent.getProperties().add(exDate); + } + + } catch (ParseException e) { + Log.e(TAG, "caught exception while parsing recurrence rule stuff from event values", e); + throw new InvalidComponentException("caught exception while parsing recurrence rule stuff from event values", + false, CalDavConstants.CALDAV_NAMESPACE, path, e); + } + + calendar.getComponents().add(vEvent); + Optional eTag = Optional.fromNullable(eventValues.getAsString(CalendarContract.Events.SYNC_DATA1)); + + return new ComponentETagPair(calendar, eTag); + } + + protected static String[] getProjectionForAttendee() { + return new String[] { + CalendarContract.Attendees.EVENT_ID, // 00 + CalendarContract.Attendees.ATTENDEE_EMAIL, // 01 + CalendarContract.Attendees.ATTENDEE_NAME, // 02 + CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, // 03 + CalendarContract.Attendees.ATTENDEE_TYPE, // 04 + CalendarContract.Attendees.ATTENDEE_STATUS, // 05 + CalendarContract.Attendees.ATTENDEE_IDENTITY, // 06 + CalendarContract.Attendees.ATTENDEE_ID_NAMESPACE // 07 + }; + } + + protected static ContentValues getValuesForAttendee(Cursor cursor) { + ContentValues values = new ContentValues(8); + + values.put(CalendarContract.Attendees.EVENT_ID, cursor.getLong(0)); + values.put(CalendarContract.Attendees.ATTENDEE_EMAIL, cursor.getString(1)); + values.put(CalendarContract.Attendees.ATTENDEE_NAME, cursor.getString(2)); + values.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, cursor.getInt(3)); + values.put(CalendarContract.Attendees.ATTENDEE_TYPE, cursor.getInt(4)); + values.put(CalendarContract.Attendees.ATTENDEE_STATUS, cursor.getInt(5)); + values.put(CalendarContract.Attendees.ATTENDEE_IDENTITY, cursor.getString(6)); + values.put(CalendarContract.Attendees.ATTENDEE_ID_NAMESPACE, cursor.getString(7)); + + return values; + } + + protected static List getValuesForAttendees(Calendar component) { + List valuesList = new LinkedList(); + VEvent vEvent = (VEvent) component.getComponent(VEvent.VEVENT); + + if (vEvent == null || vEvent.getProperties(Attendee.ATTENDEE) == null) + return valuesList; + + PropertyList attendeeList = vEvent.getProperties(Attendee.ATTENDEE); + + for (int i = 0; i < attendeeList.size(); i++) { + ContentValues values = new ContentValues(); + Attendee attendee = (Attendee) attendeeList.get(i); + if (attendee != null) { + + String email = Uri.parse(attendee.getValue()).getSchemeSpecificPart(); + Cn name = (Cn) attendee.getParameter(Cn.CN); + Role relationship = (Role) attendee.getParameter(Parameter.ROLE); + PartStat status = (PartStat) attendee.getParameter(Parameter.PARTSTAT); + + if (StringUtils.isNotEmpty(email)) + values.put(CalendarContract.Attendees.ATTENDEE_EMAIL, email); + + if (name != null) + values.put(CalendarContract.Attendees.ATTENDEE_NAME, name.getValue()); + + if (relationship != null) { + if (relationship == Role.CHAIR) { + values.put(CalendarContract.Attendees.ATTENDEE_TYPE, + CalendarContract.Attendees.TYPE_REQUIRED); + values.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, + CalendarContract.Attendees.RELATIONSHIP_ORGANIZER); + } + else if (relationship == Role.REQ_PARTICIPANT) { + values.put(CalendarContract.Attendees.ATTENDEE_TYPE, + CalendarContract.Attendees.TYPE_REQUIRED); + values.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, + CalendarContract.Attendees.RELATIONSHIP_ATTENDEE); + } + else { + values.put(CalendarContract.Attendees.ATTENDEE_TYPE, + CalendarContract.Attendees.TYPE_OPTIONAL); + values.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, + CalendarContract.Attendees.RELATIONSHIP_ATTENDEE); + } + } + else { + values.put(CalendarContract.Attendees.ATTENDEE_TYPE, + CalendarContract.Attendees.TYPE_OPTIONAL); + values.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, + CalendarContract.Attendees.RELATIONSHIP_ATTENDEE); + } + + if (status != null) { + if (status == PartStat.NEEDS_ACTION) + values.put(CalendarContract.Attendees.ATTENDEE_STATUS, + CalendarContract.Attendees.ATTENDEE_STATUS_INVITED); + else if (status == PartStat.TENTATIVE) + values.put(CalendarContract.Attendees.ATTENDEE_STATUS, + CalendarContract.Attendees.ATTENDEE_STATUS_TENTATIVE); + else if (status == PartStat.ACCEPTED) + values.put(CalendarContract.Attendees.ATTENDEE_STATUS, + CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED); + else if (status == PartStat.DECLINED) + values.put(CalendarContract.Attendees.ATTENDEE_STATUS, + CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED); + else + values.put(CalendarContract.Attendees.ATTENDEE_STATUS, + CalendarContract.Attendees.ATTENDEE_STATUS_NONE); + } + else + values.put(CalendarContract.Attendees.ATTENDEE_STATUS, + CalendarContract.Attendees.ATTENDEE_STATUS_NONE); + + valuesList.add(values); + } + } + + return valuesList; + } + + protected static void addAttendee(String path, Calendar component, ContentValues attendeeValues) + throws InvalidComponentException + { + VEvent vEvent = (VEvent) component.getComponent(VEvent.VEVENT); + if (vEvent == null) { + Log.e(TAG, "unable to add attendee to component with no VEVENT"); + throw new InvalidComponentException("unable to add attendee to component with no VEVENT", false, + CalDavConstants.CALDAV_NAMESPACE, path); + } + + String email = attendeeValues.getAsString(CalendarContract.Attendees.ATTENDEE_EMAIL); + String name = attendeeValues.getAsString(CalendarContract.Attendees.ATTENDEE_NAME); + Integer type = attendeeValues.getAsInteger(CalendarContract.Attendees.ATTENDEE_TYPE); + Integer relationship = attendeeValues.getAsInteger(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP); + Integer status = attendeeValues.getAsInteger(CalendarContract.Attendees.ATTENDEE_STATUS); + + if (StringUtils.isEmpty(email)) { + Log.e(TAG, "attendee email is null or empty"); + throw new InvalidComponentException("attendee email is null or empty", false, + CalDavConstants.CALDAV_NAMESPACE, path); + } + + try { + + Attendee attendee = new Attendee(new URI("mailto", email, null)); + ParameterList attendeeParams = attendee.getParameters(); + + attendeeParams.add(CuType.INDIVIDUAL); + + if (StringUtils.isNotEmpty(name)) + attendeeParams.add(new Cn(name)); + + if (relationship != null && relationship == CalendarContract.Attendees.RELATIONSHIP_ORGANIZER) + attendeeParams.add(Role.CHAIR); + else if (type != null && type == CalendarContract.Attendees.TYPE_REQUIRED) + attendeeParams.add(Role.REQ_PARTICIPANT); + else + attendeeParams.add(Role.OPT_PARTICIPANT); + + if (status != null) { + switch (status) { + case CalendarContract.Attendees.ATTENDEE_STATUS_INVITED: + attendeeParams.add(PartStat.NEEDS_ACTION); + break; + + case CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED: + attendeeParams.add(PartStat.ACCEPTED); + break; + + case CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED: + attendeeParams.add(PartStat.DECLINED); + break; + + case CalendarContract.Attendees.ATTENDEE_STATUS_TENTATIVE: + attendeeParams.add(PartStat.TENTATIVE); + break; + } + } + vEvent.getProperties().add(attendee); + + } catch (URISyntaxException e) { + Log.e(TAG, "caught exception while adding email to attendee", e); + throw new InvalidComponentException("caught exception while adding email to attendee", false, + CalDavConstants.CALDAV_NAMESPACE, path, e); + } + } + + protected static String [] getProjectionForReminder() { + return new String[] { + CalendarContract.Reminders.EVENT_ID, // 00 + CalendarContract.Reminders.MINUTES, // 01 + CalendarContract.Reminders.METHOD // 02 + }; + } + + protected static ContentValues getValuesForReminder(String path, Cursor cursor) + throws InvalidComponentException + { + if (!cursor.isNull(0) && !cursor.isNull(1)) { + ContentValues values = new ContentValues(3); + + values.put(CalendarContract.Reminders.EVENT_ID, cursor.getLong(0)); + values.put(CalendarContract.Reminders.MINUTES, cursor.getInt(1)); + values.put(CalendarContract.Reminders.METHOD, cursor.getInt(2)); + return values; + } + + Log.e(TAG, "reminder event id or minutes is null"); + throw new InvalidComponentException("reminder event id or minutes is null", false, + CalDavConstants.CALDAV_NAMESPACE, path); + } + + protected static List getValuesForReminders(Calendar component) { + List valueList = new LinkedList(); + VEvent vEvent = (VEvent) component.getComponent(VEvent.VEVENT); + VToDo vToDo = (VToDo) component.getComponent(VToDo.VTODO); + ComponentList vAlarms; + + if (vEvent != null) + vAlarms = vEvent.getAlarms(); + else if (vToDo != null) + vAlarms = vToDo.getAlarms(); + else + return valueList; + + for (int i = 0; i < vAlarms.size(); i++) { + VAlarm vAlarm = (VAlarm) vAlarms.get(i); + if (vAlarm != null) { + + Trigger trigger = vAlarm.getTrigger(); + if (trigger != null && trigger.getDuration() != null) { + ContentValues values = new ContentValues(); + values.put(CalendarContract.Reminders.MINUTES, (trigger.getDuration().getMinutes())); + values.put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_DEFAULT); + valueList.add(values); + } + } + } + + return valueList; + } + + // TODO: can we support more alarm types? + protected static void addReminder(String path, Calendar component, ContentValues reminderValues) + throws InvalidComponentException + { + Integer minutes = reminderValues.getAsInteger(CalendarContract.Reminders.MINUTES); + + if (minutes != null) { + VAlarm vAlarm = new VAlarm(new Dur(0, 0, -minutes, 0)); + PropertyList alarmProps = vAlarm.getProperties(); + + alarmProps.add(Action.DISPLAY); + + VEvent vEvent = (VEvent) component.getComponent(VEvent.VEVENT); + VToDo vToDo = (VToDo) component.getComponent(VEvent.VTODO); + + if (vEvent != null && vEvent.getSummary() != null) { + alarmProps.add(new Description(vEvent.getSummary().getValue())); + vEvent.getAlarms().add(vAlarm); + } + else if (vToDo != null && vToDo.getSummary() != null) { + alarmProps.add(new Description(vToDo.getSummary().getValue())); + vToDo.getAlarms().add(vAlarm); + } + } + else { + Log.e(TAG, "reminder minutes is null"); + throw new InvalidComponentException("reminder minutes is null", false, + CalDavConstants.CALDAV_NAMESPACE, path); + } + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/HidingCalDavCollection.java b/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/HidingCalDavCollection.java new file mode 100644 index 0000000..b01c054 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/HidingCalDavCollection.java @@ -0,0 +1,348 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.calendar; + +import android.util.Log; + +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.webdav.caldav.CalDavConstants; + +import net.fortuna.ical4j.data.CalendarBuilder; +import net.fortuna.ical4j.data.CalendarOutputter; +import net.fortuna.ical4j.data.ParserException; +import net.fortuna.ical4j.model.Calendar; +import net.fortuna.ical4j.model.Date; +import net.fortuna.ical4j.model.ValidationException; +import net.fortuna.ical4j.model.component.VEvent; +import net.fortuna.ical4j.model.component.VToDo; +import net.fortuna.ical4j.model.property.CalScale; +import net.fortuna.ical4j.model.property.DtEnd; +import net.fortuna.ical4j.model.property.ProdId; +import net.fortuna.ical4j.model.property.Version; +import net.fortuna.ical4j.model.property.XProperty; +import org.anhonesteffort.flock.crypto.InvalidMacException; +import org.anhonesteffort.flock.crypto.MasterCipher; +import org.anhonesteffort.flock.sync.HidingDavCollection; +import org.anhonesteffort.flock.sync.HidingDavCollectionMixin; +import org.anhonesteffort.flock.sync.HidingUtil; +import org.anhonesteffort.flock.sync.OwsWebDav; +import org.anhonesteffort.flock.webdav.ComponentETagPair; +import org.anhonesteffort.flock.webdav.InvalidComponentException; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.anhonesteffort.flock.webdav.caldav.CalDavCollection; +import org.anhonesteffort.flock.webdav.caldav.CalDavStore; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringReader; +import java.security.GeneralSecurityException; +import java.util.LinkedList; +import java.util.List; + +/** + * Programmer: rhodey + */ +public class HidingCalDavCollection extends CalDavCollection implements HidingDavCollection { + + private static final String TAG = "org.anhonesteffort.flock.sync.calendar.HidingCalDavCollection"; + + private static final String PROPERTY_NAME_FLOCK_HIDDEN_CALENDAR = "X-FLOCK-HIDDEN-CALENDAR"; + + protected static final String PROPERTY_NAME_HIDDEN_COLOR = "X-FLOCK-HIDDEN-CALENDAR-COLOR"; + protected static final DavPropertyName PROPERTY_HIDDEN_COLOR = DavPropertyName.create( + PROPERTY_NAME_HIDDEN_COLOR, + OwsWebDav.NAMESPACE + ); + + private MasterCipher masterCipher; + private HidingDavCollectionMixin delegate; + + protected HidingCalDavCollection(CalDavStore calDavStore, String path, MasterCipher masterCipher) { + super(calDavStore, path, new DavPropertySet()); + + this.masterCipher = masterCipher; + this.delegate = new HidingDavCollectionMixin(this, masterCipher); + } + + protected HidingCalDavCollection(CalDavCollection calDavCollection, MasterCipher masterCipher) { + super((CalDavStore) calDavCollection.getStore(), + calDavCollection.getPath(), + calDavCollection.getProperties()); + + this.masterCipher = masterCipher; + this.delegate = new HidingDavCollectionMixin(this, masterCipher); + } + + @Override + protected DavPropertyNameSet getPropertyNamesForFetch() { + DavPropertyNameSet calendarProps = super.getPropertyNamesForFetch(); + DavPropertyNameSet hidingProps = delegate.getPropertyNamesForFetch(); + + calendarProps.addAll(hidingProps); + calendarProps.add(PROPERTY_HIDDEN_COLOR); + + return calendarProps; + } + + @Override + public Optional getHiddenDisplayName() + throws PropertyParseException, InvalidMacException, + GeneralSecurityException, IOException + { + return delegate.getHiddenDisplayName(); + } + + @Override + public void setHiddenDisplayName(String displayName) + throws DavException, IOException, + InvalidMacException, GeneralSecurityException + { + delegate.setHiddenDisplayName(displayName); + } + + public Optional getHiddenColor() + throws PropertyParseException, InvalidMacException, + GeneralSecurityException, IOException + { + Optional hiddenColor = getProperty(PROPERTY_HIDDEN_COLOR, String.class); + + if (!hiddenColor.isPresent()) + return getColor(); + + String hiddenColorString = HidingUtil.decodeAndDecryptIfNecessary(masterCipher, hiddenColor.get()); + + return Optional.of(Integer.valueOf(hiddenColorString)); + } + + public void setHiddenColor(Integer color) + throws DavException, IOException, + InvalidMacException, GeneralSecurityException + { + String hiddenColorString = HidingUtil.encryptEncodeAndPrefix(masterCipher, color.toString()); + + DavPropertySet updateProperties = new DavPropertySet(); + updateProperties.add(new DefaultDavProperty(PROPERTY_HIDDEN_COLOR, hiddenColorString)); + + patchProperties(updateProperties, new DavPropertyNameSet()); + } + + public Optional getKeyMaterialSalt() throws PropertyParseException { + return delegate.getKeyMaterialSalt(); + } + + public void setKeyMaterialSalt(String keyMaterialSalt) + throws DavException, IOException + { + delegate.setKeyMaterialSalt(keyMaterialSalt); + } + + @Override + public Optional getEncryptedKeyMaterial() throws PropertyParseException { + return delegate.getEncryptedKeyMaterial(); + } + + @Override + public void setEncryptedKeyMaterial(String encryptedKeyMaterial) + throws DavException, IOException + { + delegate.setEncryptedKeyMaterial(encryptedKeyMaterial); + } + + @Override + public Optional> getHiddenComponent(String uid) + throws InvalidComponentException, DavException, + InvalidMacException, GeneralSecurityException, IOException + { + Optional> originalComponentPair = super.getComponent(uid); + if (!originalComponentPair.isPresent()) + return Optional.absent(); + + Calendar exposedComponent = originalComponentPair.get().getComponent(); + XProperty protectedComponent = (XProperty) exposedComponent.getProperty(PROPERTY_NAME_FLOCK_HIDDEN_CALENDAR); + + if (protectedComponent == null) + return originalComponentPair; + + String recoveredComponentText = HidingUtil.decodeAndDecryptIfNecessary(masterCipher, protectedComponent.getValue()); + StringReader stringReader = new StringReader(recoveredComponentText.replace("\n ", "")); + CalendarBuilder calendarBuilder = new CalendarBuilder(); + + try { + + Calendar recoveredComponent = calendarBuilder.build(stringReader); + return Optional.of(new ComponentETagPair(recoveredComponent, + originalComponentPair.get().getETag())); + + } catch (ParserException e) { + Log.e(TAG, "caught exception while trying to build from hidden component", e); + throw new InvalidComponentException("caught exception while trying to build from hidden component", + true, CalDavConstants.CALDAV_NAMESPACE, getPath(), uid, e); + } + } + + @Override + public List> getHiddenComponents() + throws InvalidComponentException, DavException, + InvalidMacException, GeneralSecurityException, IOException + { + List> exposedComponentPairs = super.getComponents(); + List> recoveredComponentPairs = new LinkedList>(); + + for (ComponentETagPair exposedComponentPair : exposedComponentPairs) { + Calendar exposedComponent = exposedComponentPair.getComponent(); + XProperty protectedComponent = (XProperty) exposedComponent.getProperty(PROPERTY_NAME_FLOCK_HIDDEN_CALENDAR); + + if (protectedComponent == null) + recoveredComponentPairs.add(exposedComponentPair); + else { + String recoveredComponentText = HidingUtil.decodeAndDecryptIfNecessary(masterCipher, protectedComponent.getValue()); + StringReader stringReader = new StringReader(recoveredComponentText.replace("\n ", "")); + CalendarBuilder calendarBuilder = new CalendarBuilder(); + + try { + + Calendar recoveredComponent = calendarBuilder.build(stringReader); + recoveredComponentPairs.add(new ComponentETagPair(recoveredComponent, + exposedComponentPair.getETag())); + + } catch (ParserException e) { + Log.e(TAG, "caught exception while trying to build from hidden component", e); + throw new InvalidComponentException("caught exception while trying to build from hidden component", + true, CalDavConstants.CALDAV_NAMESPACE, getPath(), e); + } + } + } + + return recoveredComponentPairs; + } + + // NOTICE: All events starting within a given month will appear to start on the first day + // NOTICE... of the month and end on the last day of this month... is this acceptable??? + protected void putHiddenComponentToServer(Calendar exposedComponent, Optional ifMatchETag) + throws InvalidComponentException, GeneralSecurityException, IOException, DavException + { + exposedComponent.getProperties().remove(ProdId.PRODID); + exposedComponent.getProperties().add(new ProdId(((CalDavStore)getStore()).getProductId())); + + Calendar protectedComponent = new Calendar(); + protectedComponent.getProperties().add(Version.VERSION_2_0); + protectedComponent.getProperties().add(CalScale.GREGORIAN); + + java.util.Calendar calendar = java.util.Calendar.getInstance(); + VEvent exposedEvent = (VEvent) exposedComponent.getComponent(VEvent.VEVENT); + VToDo exposedToDo = (VToDo ) exposedComponent.getComponent(VEvent.VTODO); + + if (exposedEvent != null) { + if (exposedEvent.getUid() == null || exposedEvent.getUid().getValue() == null) { + Log.e(TAG, "was given a VEVENT with no UID"); + throw new InvalidComponentException("Cannot put an iCal to server without UID!", + false, CalDavConstants.CALDAV_NAMESPACE, getPath()); + } + + Date startDate = exposedEvent.getStartDate().getDate(); + calendar.setTime(new java.util.Date(startDate.getTime())); + + calendar.set(java.util.Calendar.DAY_OF_MONTH, 1); + calendar.set(java.util.Calendar.HOUR, 1); + calendar.set(java.util.Calendar.MINUTE, 1); + calendar.set(java.util.Calendar.SECOND, 1); + calendar.set(java.util.Calendar.MILLISECOND, 1); + + Date approximateEndDate = new Date(calendar.getActualMaximum(java.util.Calendar.DAY_OF_MONTH)); + DtEnd approximateDtEnd = new DtEnd(approximateEndDate); + + VEvent protectedEvent = new VEvent(new Date(calendar.getTime()), "Open Whisper Systems - Flock"); + protectedEvent.getProperties().add(approximateDtEnd); + protectedEvent.getProperties().add(exposedEvent.getUid()); + + protectedComponent.getComponents().add(protectedEvent); + } + else if (exposedToDo != null) { + if (exposedToDo.getUid() == null || exposedToDo.getUid().getValue() == null) { + Log.e(TAG, "was given a VTODO with no UID"); + throw new InvalidComponentException("Cannot put an iCal to server without UID!", + false, CalDavConstants.CALDAV_NAMESPACE, getPath()); + } + + Date startDate = exposedToDo.getStartDate().getDate(); + calendar.setTime(new java.util.Date(startDate.getTime())); + + calendar.set(java.util.Calendar.DAY_OF_MONTH, 1); + calendar.set(java.util.Calendar.HOUR, 1); + calendar.set(java.util.Calendar.MINUTE, 1); + calendar.set(java.util.Calendar.SECOND, 1); + calendar.set(java.util.Calendar.MILLISECOND, 1); + + Date approximateEndDate = new Date(calendar.getActualMaximum(java.util.Calendar.DAY_OF_MONTH)); + DtEnd approximateDtEnd = new DtEnd(approximateEndDate); + + VToDo protectedToDo = new VToDo(new Date(calendar.getTime()), "Open Whisper Systems - Flock"); + protectedToDo.getProperties().add(approximateDtEnd); + protectedToDo.getProperties().add(exposedToDo.getUid()); + + protectedComponent.getComponents().add(protectedToDo); + } + else { + Log.e(TAG, "was given an calendar component containing neither VEVENT or VTODO"); + throw new InvalidComponentException("was given an calendar component containing neither VEVENT or VTODO", + false, CalDavConstants.CALDAV_NAMESPACE, getPath()); + } + + CalendarOutputter calendarOutputter = new CalendarOutputter(); + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + + try { + + calendarOutputter.output(exposedComponent, byteStream); + String protectedComponentData = HidingUtil.encryptEncodeAndPrefix(masterCipher, byteStream.toString()); + XProperty xSecureSyncHiddenProp = new XProperty(PROPERTY_NAME_FLOCK_HIDDEN_CALENDAR, protectedComponentData); + protectedComponent.getProperties().add(xSecureSyncHiddenProp); + + super.putComponentToServer(protectedComponent, ifMatchETag); + + } catch (ValidationException e) { + Log.e(TAG, "caught exception while trying to output component to byte stream", e); + throw new InvalidComponentException("Caught exception while trying to output component to byte stream", + false, CalDavConstants.CALDAV_NAMESPACE, getPath(), e); + } + } + + @Override + public void addHiddenComponent(Calendar component) + throws InvalidComponentException, DavException, GeneralSecurityException, IOException + { + putHiddenComponentToServer(component, Optional.absent()); + fetchProperties(); + } + + @Override + public void updateHiddenComponent(ComponentETagPair component) + throws InvalidComponentException, DavException, GeneralSecurityException, IOException + { + putHiddenComponentToServer(component.getComponent(), component.getETag()); + fetchProperties(); + } +} \ No newline at end of file diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/HidingCalDavStore.java b/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/HidingCalDavStore.java new file mode 100644 index 0000000..88ff9f4 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/HidingCalDavStore.java @@ -0,0 +1,187 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.calendar; + +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.webdav.caldav.CalDavConstants; + +import org.anhonesteffort.flock.crypto.MasterCipher; +import org.anhonesteffort.flock.sync.HidingDavStore; +import org.anhonesteffort.flock.sync.HidingUtil; +import org.anhonesteffort.flock.webdav.DavClient; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.anhonesteffort.flock.webdav.caldav.CalDavCollection; +import org.anhonesteffort.flock.webdav.caldav.CalDavStore; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.client.methods.PropFindMethod; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.LinkedList; +import java.util.List; + +/** + * Programmer: rhodey + */ +public class HidingCalDavStore implements HidingDavStore { + + private MasterCipher masterCipher; + private CalDavStore calDavStore; + + public HidingCalDavStore(MasterCipher masterCipher, + String hostHREF, + String username, + String password, + Optional currentUserPrincipal, + Optional calendarHomeSet) + throws DavException, IOException + { + this.masterCipher = masterCipher; + this.calDavStore = new CalDavStore(hostHREF, username, password, currentUserPrincipal, calendarHomeSet); + } + + public HidingCalDavStore(MasterCipher masterCipher, + DavClient client, + Optional currentUserPrincipal, + Optional calendarHomeSet) { + this.masterCipher = masterCipher; + this.calDavStore = new CalDavStore(client, currentUserPrincipal, calendarHomeSet); + } + + @Override + public String getHostHREF() { + return calDavStore.getHostHREF(); + } + + public Optional getCalendarHomeSet() + throws PropertyParseException, DavException, IOException + { + return calDavStore.getCalendarHomeSet(); + } + + @Override + public Optional getCollection(String path) throws DavException, IOException { + HidingCalDavCollection targetCollection = new HidingCalDavCollection(calDavStore, path, masterCipher); + DavPropertyNameSet collectionProps = targetCollection.getPropertyNamesForFetch(); + PropFindMethod propFindMethod = new PropFindMethod(path, collectionProps, PropFindMethod.DEPTH_0); + + try { + + calDavStore.getClient().execute(propFindMethod); + + MultiStatus multiStatus = propFindMethod.getResponseBodyAsMultiStatus(); + MultiStatusResponse[] responses = multiStatus.getResponses(); + + List returnedCollections = CalDavStore.getCollectionsFromMultiStatusResponses(calDavStore, responses); + + if (returnedCollections.size() == 0) + Optional.absent(); + + return Optional.of(new HidingCalDavCollection(returnedCollections.get(0), masterCipher)); + + } catch (DavException e) { + + if (e.getErrorCode() == DavServletResponse.SC_NOT_FOUND) + return Optional.absent(); + + throw e; + + } finally { + propFindMethod.releaseConnection(); + } + } + + @Override + public List getCollections() + throws PropertyParseException, DavException, IOException + { + Optional calHomeSetUri = getCalendarHomeSet(); + if (!calHomeSetUri.isPresent()) + throw new PropertyParseException("No calendar-home-set property found for user.", + getHostHREF(), CalDavConstants.PROPERTY_NAME_CALENDAR_HOME_SET); + + HidingCalDavCollection hack = new HidingCalDavCollection(calDavStore, "hack", masterCipher); + DavPropertyNameSet calendarProps = hack.getPropertyNamesForFetch(); + + PropFindMethod method = new PropFindMethod(getHostHREF().concat(calHomeSetUri.get()), + calendarProps, + PropFindMethod.DEPTH_1); + + try { + + calDavStore.getClient().execute(method); + + MultiStatus multiStatus = method.getResponseBodyAsMultiStatus(); + MultiStatusResponse[] responses = multiStatus.getResponses(); + + List hidingCollections = new LinkedList(); + List collections = CalDavStore.getCollectionsFromMultiStatusResponses(calDavStore, responses); + + for (CalDavCollection collection : collections) + hidingCollections.add(new HidingCalDavCollection(collection, masterCipher)); + + return hidingCollections; + + } finally { + method.releaseConnection(); + } + } + + @Override + public void addCollection(String path) + throws DavException, IOException, GeneralSecurityException + { + calDavStore.addCollection(path); + } + + public void addCollection(String path, + String displayName, + Integer color) + throws DavException, IOException, GeneralSecurityException + { + DavPropertySet properties = new DavPropertySet(); + String hiddenDisplayName = HidingUtil.encryptEncodeAndPrefix(masterCipher, displayName); + String hiddenColor = HidingUtil.encryptEncodeAndPrefix(masterCipher, color.toString()); + + properties.add(new DefaultDavProperty(DavPropertyName.DISPLAYNAME, hiddenDisplayName)); + properties.add(new DefaultDavProperty(HidingCalDavCollection.PROPERTY_HIDDEN_COLOR, hiddenColor)); + + calDavStore.addCollection(path, properties); + } + + @Override + public void removeCollection(String path) throws DavException, IOException { + calDavStore.removeCollection(path); + } + + public void releaseConnections() { + calDavStore.closeHttpConnection(); + } + + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/LocalCalendarStore.java b/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/LocalCalendarStore.java new file mode 100644 index 0000000..963027b --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/LocalCalendarStore.java @@ -0,0 +1,271 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.calendar; + +import android.accounts.Account; +import android.content.ContentProviderClient; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; +import android.provider.CalendarContract; +import android.util.Log; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.sync.LocalComponentStore; + +import java.util.LinkedList; +import java.util.List; + +/** + * Programmer: rhodey + */ +public class LocalCalendarStore implements LocalComponentStore { + + private static final String TAG = "org.anhonesteffort.flock.sync.calendar.LocalCalendarStore"; + + public static final int MAXIMUM_COLLECTIONS = 100; // is this reasonable? + + private ContentProviderClient client; + private Account account; + + public LocalCalendarStore(ContentProviderClient client, Account account) { + this.client = client; + this.account = account; + } + + public LocalCalendarStore(Context context, Account account) { + this.account = account; + client = context.getContentResolver().acquireContentProviderClient + (CalendarsSyncScheduler.CONTENT_AUTHORITY); + } + + @Override + public Optional getCollection(String remotePath) + throws RemoteException + { + final String[] PROJECTION = new String[]{CalendarContract.Calendars._ID, + CalendarContract.Calendars.NAME}; + final String SELECTION = CalendarContract.Calendars.DELETED + "=0 " + + "AND " + CalendarContract.Calendars.SYNC_EVENTS + "=1 " + + "AND " + CalendarContract.Calendars.NAME + "=?"; + final String[] SELECTION_ARGS = new String[]{remotePath}; + + Cursor cursor = client.query(LocalEventCollection.getCollectionsUri(account), + PROJECTION, + SELECTION, + SELECTION_ARGS, + null); + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + List collections = new LinkedList(); + + if (cursor.moveToNext()) + collections.add(new LocalEventCollection(client, + account, + cursor.getLong(0), + cursor.getString(1))); + cursor.close(); + + if (collections.size() > 0) + return Optional.of(collections.get(0)); + + return Optional.absent(); + } + + @Override + public void addCollection(String remotePath) throws RemoteException { + addCollection(remotePath, "UNKNOWN", 0xffffffff); + } + + @Override + public void addCollection(String remotePath, String displayName) throws RemoteException { + addCollection(remotePath, displayName, 0xffffffff); + } + + public void addCollection(String remotePath, String displayName, int color) throws RemoteException { + if (getCollection(remotePath).isPresent()) + { + Log.w(TAG, "attempted to create duplicate collection at " + remotePath); + throw new RemoteException("Collection already exists!"); + } + + ContentValues values = new ContentValues(); + + values.put(CalendarContract.Calendars.ACCOUNT_NAME, account.name); + values.put(CalendarContract.Calendars.ACCOUNT_TYPE, account.type); + + values.put(LocalEventCollection.COLUMN_NAME_COLLECTION_COPIED, 0); + + values.put(CalendarContract.Calendars.NAME, remotePath); + values.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, displayName); + values.put(CalendarContract.Calendars.CALENDAR_COLOR, color); + + values.put(CalendarContract.Calendars.OWNER_ACCOUNT, account.name); + values.put(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, + CalendarContract.Calendars.CAL_ACCESS_OWNER); + + values.put(CalendarContract.Calendars.ALLOWED_REMINDERS, + CalendarContract.Reminders.METHOD_ALERT); + + values.put(CalendarContract.Calendars.ALLOWED_AVAILABILITY, + CalendarContract.Events.AVAILABILITY_BUSY + "," + + CalendarContract.Events.AVAILABILITY_FREE); + + values.put(CalendarContract.Calendars.ALLOWED_ATTENDEE_TYPES, + CalendarContract.Attendees.TYPE_NONE + "," + + CalendarContract.Attendees.TYPE_OPTIONAL + "," + + CalendarContract.Attendees.TYPE_REQUIRED + "," + + CalendarContract.Attendees.TYPE_RESOURCE); + + values.put(CalendarContract.Calendars.CAN_ORGANIZER_RESPOND, 0); + values.put(CalendarContract.Calendars.CAN_MODIFY_TIME_ZONE, 1); + + values.put(CalendarContract.Calendars.SYNC_EVENTS, 1); + values.put(CalendarContract.Calendars.VISIBLE, 1); + + client.insert(LocalEventCollection.getCollectionsUri(account), values); + } + + @Override + public void removeCollection(String remotePath) throws RemoteException{ + final String SELECTION = CalendarContract.Calendars.NAME + "=?"; + final String[] SELECTION_ARGS = new String[]{remotePath}; + + client.delete(LocalEventCollection.getCollectionsUri(account), + SELECTION, + SELECTION_ARGS); + } + + public void setCollectionCopied(Long localId, boolean isCopied) throws RemoteException { + final String COLLECTION_SELECTION = CalendarContract.Calendars._ID + "=?"; + final String[] COLLECTION_SELECTION_ARGS = new String[] {localId.toString()}; + final Uri COLLECTION_URI = LocalEventCollection.getCollectionsUri(account); + + ContentValues updateValues = new ContentValues(); + updateValues.put(LocalEventCollection.COLUMN_NAME_COLLECTION_COPIED, isCopied ? 1 : 0); + + client.update(COLLECTION_URI, updateValues, COLLECTION_SELECTION, COLLECTION_SELECTION_ARGS); + } + + public void setCollectionPath(Long localId, String path) throws RemoteException { + final String COLLECTION_SELECTION = CalendarContract.Calendars._ID + "=?"; + final String[] COLLECTION_SELECTION_ARGS = new String[] {localId.toString()}; + final Uri COLLECTION_URI = LocalEventCollection.getCollectionsUri(account); + + ContentValues updateValues = new ContentValues(); + updateValues.put(CalendarContract.Calendars.NAME, path); + + client.update(COLLECTION_URI, updateValues, COLLECTION_SELECTION, COLLECTION_SELECTION_ARGS); + } + + @Override + public List getCollections() throws RemoteException { + final String[] PROJECTION = + new String[]{CalendarContract.Calendars._ID, CalendarContract.Calendars.NAME}; + + String SELECTION = CalendarContract.Calendars.DELETED + "=0 AND " + + CalendarContract.Calendars.SYNC_EVENTS + "=1"; + + if (account.type.equals(DavAccount.SYNC_ACCOUNT_TYPE)) + SELECTION += " AND " + LocalEventCollection.COLUMN_NAME_COLLECTION_COPIED + "=0"; + + Cursor cursor = client.query(LocalEventCollection.getCollectionsUri(account), + PROJECTION, + SELECTION, null, null); + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + List collections = new LinkedList(); + + while (cursor.moveToNext()) + collections.add(new LocalEventCollection(client, + account, + cursor.getLong(0), + cursor.getString(1))); + cursor.close(); + + return collections; + } + + public List getCollectionsIgnoreSync() throws RemoteException { + final String[] PROJECTION = + new String[]{CalendarContract.Calendars._ID, CalendarContract.Calendars.NAME}; + + String SELECTION = CalendarContract.Calendars.DELETED + "=0"; + + if (account.type.equals(DavAccount.SYNC_ACCOUNT_TYPE)) + SELECTION += " AND " + LocalEventCollection.COLUMN_NAME_COLLECTION_COPIED + "=0"; + + Cursor cursor = client.query(LocalEventCollection.getCollectionsUri(account), + PROJECTION, + SELECTION, null, null); + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + List collections = new LinkedList(); + + while (cursor.moveToNext()) + collections.add(new LocalEventCollection(client, + account, + cursor.getLong(0), + cursor.getString(1))); + cursor.close(); + + return collections; + } + + public List getCopiedCollections() throws RemoteException { + final String[] PROJECTION = + new String[]{CalendarContract.Calendars._ID, CalendarContract.Calendars.NAME}; + + final String SELECTION = LocalEventCollection.COLUMN_NAME_COLLECTION_COPIED + "=1 AND " + + CalendarContract.Calendars.DELETED + "=0 AND " + + CalendarContract.Calendars.SYNC_EVENTS + "=1"; + + if (!account.type.equals(DavAccount.SYNC_ACCOUNT_TYPE)) + throw new RemoteException("Unable to determine which collections are copied!"); + + Cursor cursor = client.query(LocalEventCollection.getCollectionsUri(account), + PROJECTION, + SELECTION, null, null); + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + List collections = new LinkedList(); + + while (cursor.moveToNext()) + collections.add(new LocalEventCollection(client, + account, + cursor.getLong(0), + cursor.getString(1))); + cursor.close(); + + return collections; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/LocalEventCollection.java b/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/LocalEventCollection.java new file mode 100644 index 0000000..be972bb --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/calendar/LocalEventCollection.java @@ -0,0 +1,748 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.calendar; + +import android.accounts.Account; +import android.content.ContentProviderClient; +import android.content.ContentProviderOperation; +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; + +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.webdav.caldav.CalDavConstants; + +import net.fortuna.ical4j.model.Calendar; +import net.fortuna.ical4j.model.ConstraintViolationException; +import net.fortuna.ical4j.model.PropertyList; +import net.fortuna.ical4j.model.TimeZoneRegistry; +import net.fortuna.ical4j.model.TimeZoneRegistryFactory; +import net.fortuna.ical4j.model.component.VEvent; +import net.fortuna.ical4j.model.component.VTimeZone; +import net.fortuna.ical4j.model.property.Attendee; +import net.fortuna.ical4j.model.property.Organizer; +import net.fortuna.ical4j.model.property.Uid; +import net.fortuna.ical4j.model.property.Version; +import net.fortuna.ical4j.util.Calendars; +import org.anhonesteffort.flock.sync.AbstractLocalComponentCollection; +import org.anhonesteffort.flock.sync.InvalidLocalComponentException; +import org.anhonesteffort.flock.webdav.ComponentETagPair; +import org.anhonesteffort.flock.webdav.InvalidComponentException; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; + +/** + * Programmer: rhodey + */ +public class LocalEventCollection extends AbstractLocalComponentCollection { + + private static final String TAG = "org.anhonesteffort.flock.sync.calendar.LocalEventCollection"; + + private static final String COLUMN_NAME_COLLECTION_C_TAG = CalendarContract.Calendars.CAL_SYNC2; + private static final String COLUMN_NAME_COLLECTION_ORDER = CalendarContract.Calendars.CAL_SYNC3; + protected static final String COLUMN_NAME_COLLECTION_COPIED = CalendarContract.Calendars.CAL_SYNC4; + + public LocalEventCollection(ContentProviderClient client, + Account account, + Long localId, + String remotePath) + { + super(client, account, remotePath, localId); + } + + public static Uri getSyncAdapterUri(Uri base, Account account) { + return base.buildUpon() + .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, account.name) + .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, account.type) + .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") + .build(); + } + + @Override + protected Uri getSyncAdapterUri(Uri base) { + return getSyncAdapterUri(base, account); + } + + protected static Uri getCollectionsUri(Account account) { + return getSyncAdapterUri(CalendarContract.Calendars.CONTENT_URI, account); + } + + private Uri getCollectionUri() { + return ContentUris.withAppendedId(getCollectionsUri(account), localId); + } + + @Override + protected Uri getUriForComponents() { + return getSyncAdapterUri(CalendarContract.Events.CONTENT_URI); + } + + private Uri getUriForAttendees() { + return getSyncAdapterUri(CalendarContract.Attendees.CONTENT_URI); + } + + private Uri getUriForReminders() { + return getSyncAdapterUri(CalendarContract.Reminders.CONTENT_URI); + } + + protected String getColumnNameCollectionRemotePath() { + return CalendarContract.Calendars.NAME; + } + + @Override + protected String getColumnNameCollectionLocalId() { + return CalendarContract.Events.CALENDAR_ID; + } + + @Override + protected String getColumnNameComponentLocalId() { + return CalendarContract.Events._ID; + } + + @Override + protected String getColumnNameComponentUid() { + return CalendarContract.Events._SYNC_ID; + } + + @Override + protected String getColumnNameComponentETag() { + return CalendarContract.Events.SYNC_DATA1; + } + + @Override + protected String getColumnNameDirty() { + return CalendarContract.Events.DIRTY; + } + + @Override + protected String getColumnNameDeleted() { + return CalendarContract.Events.DELETED; + } + + @Override + public List getNewComponentIds() throws RemoteException { + final String[] PROJECTION = new String[]{getColumnNameComponentLocalId()}; + final String SELECTION = "(" + getColumnNameComponentUid() + " IS NULL OR " + + CalendarContract.Events.SYNC_DATA2 + " > 0) AND " + + getColumnNameCollectionLocalId() + "=" + localId; + + Cursor cursor = client.query(getUriForComponents(), PROJECTION, SELECTION, null, null); + List newIds = new LinkedList(); + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + while (cursor.moveToNext()) + newIds.add(cursor.getLong(0)); + cursor.close(); + + return newIds; + } + + @Override + public void cleanComponent(Long localId) { + Log.d(TAG, "cleanComponent() localId " + localId); + + pendingOperations.add(ContentProviderOperation + .newUpdate(ContentUris.withAppendedId(getUriForComponents(), localId)) + .withValue(getColumnNameDirty(), 0) + .withValue(CalendarContract.Events.SYNC_DATA2, null) + .build()); + } + + @Override + public Optional getDisplayName() throws RemoteException { + final String[] PROJECTION = new String[]{CalendarContract.Calendars.CALENDAR_DISPLAY_NAME}; + + Cursor cursor = client.query(getCollectionUri(), PROJECTION, null, null, null); + String displayName = null; + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + if (cursor.moveToNext()) + displayName = cursor.getString(0); + cursor.close(); + + return Optional.fromNullable(displayName); + } + + @Override + public void setDisplayName(String displayName) { + pendingOperations.add(ContentProviderOperation.newUpdate(getCollectionUri()) + .withValue(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, displayName) + .build()); + } + + public Optional getColor() throws RemoteException { + final String[] PROJECTION = new String[]{CalendarContract.Calendars.CALENDAR_COLOR}; + + Cursor cursor = client.query(getCollectionUri(), PROJECTION, null, null, null); + Integer color = null; + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + if (cursor.moveToNext()) + color = cursor.getInt(0); + cursor.close(); + + if (color == null) + return Optional.absent(); + + return Optional.of(color); + } + + public void setColor(int color) { + pendingOperations.add(ContentProviderOperation.newUpdate(getCollectionUri()) + .withValue(CalendarContract.Calendars.CALENDAR_COLOR, color) + .build()); + } + + @Override + public Optional getCTag() throws RemoteException { + final String[] PROJECTION = new String[]{COLUMN_NAME_COLLECTION_C_TAG}; + + Cursor cursor = client.query(getCollectionUri(), PROJECTION, null, null, null); + String cTag = null; + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + if (cursor.moveToNext()) + cTag = cursor.getString(0); + cursor.close(); + + return Optional.fromNullable(cTag); + } + + @Override + public void setCTag(String cTag) throws RemoteException { + pendingOperations.add(ContentProviderOperation.newUpdate(getCollectionUri()) + .withValue(COLUMN_NAME_COLLECTION_C_TAG, cTag) + .build()); + } + + public Optional getTimeZone() throws RemoteException { + final String[] PROJECTION = new String[]{CalendarContract.Calendars.CALENDAR_TIME_ZONE}; + + Cursor cursor = client.query(getCollectionUri(), PROJECTION, null, null, null); + String timeZoneId = null; + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + if (cursor.moveToNext()) + timeZoneId = cursor.getString(0); + cursor.close(); + + if (timeZoneId != null) { + Calendar calendar = new Calendar(); + TimeZoneRegistry registry = TimeZoneRegistryFactory.getInstance().createRegistry(); + VTimeZone vTimeZone = registry.getTimeZone(timeZoneId).getVTimeZone(); + + calendar.getProperties().add(Version.VERSION_2_0); + calendar.getComponents().add(vTimeZone); + + return Optional.of(calendar); + } + + return Optional.absent(); + } + + public void setTimeZone(Calendar timezone) throws InvalidComponentException { + VTimeZone vTimeZone = (VTimeZone) timezone.getComponent(VTimeZone.VTIMEZONE); + + if (vTimeZone != null && vTimeZone.getTimeZoneId() != null) { + pendingOperations.add(ContentProviderOperation.newUpdate(getCollectionUri()) + .withValue(CalendarContract.Calendars.CALENDAR_TIME_ZONE, vTimeZone.getTimeZoneId().getValue()) + .build()); + } + else + throw new InvalidComponentException("Calendar object must contain a valid VTimeZone component.", + true, CalDavConstants.CALDAV_NAMESPACE, getPath()); + } + + public Optional getOrder() throws RemoteException { + final String[] PROJECTION = new String[]{COLUMN_NAME_COLLECTION_ORDER}; + + Cursor cursor = client.query(getCollectionUri(), PROJECTION, null, null, null); + Integer order = null; + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + if (cursor.moveToNext()) + order = cursor.getInt(0); + cursor.close(); + + return Optional.fromNullable(order); + } + + public void setOrder(Integer order) { + pendingOperations.add(ContentProviderOperation.newUpdate(getCollectionUri()) + .withValue(COLUMN_NAME_COLLECTION_ORDER, order) + .build()); + } + + private void addAttendees(Long eventId, Calendar component) + throws InvalidComponentException, RemoteException + { + String SELECTION = CalendarContract.Attendees.EVENT_ID + "=?"; + String[] SELECTION_ARGS = new String[]{eventId.toString()}; + + Cursor cursor = client.query(getUriForAttendees(), + EventFactory.getProjectionForAttendee(), + SELECTION, + SELECTION_ARGS, + null); + + while (cursor.moveToNext()) { + ContentValues attendeeValues = EventFactory.getValuesForAttendee(cursor); + EventFactory.addAttendee(getPath(), component, attendeeValues); + } + cursor.close(); + } + + private void addReminders(Long eventId, Calendar component) + throws InvalidComponentException, RemoteException + { + String SELECTION = CalendarContract.Reminders.EVENT_ID + "=?"; + String[] SELECTION_ARGS = new String[]{eventId.toString()}; + + Cursor cursor = client.query(getUriForReminders(), + EventFactory.getProjectionForReminder(), + SELECTION, + SELECTION_ARGS, + null); + + while (cursor.moveToNext()) { + ContentValues reminderValues = EventFactory.getValuesForReminder(getPath(), cursor); + EventFactory.addReminder(getPath(), component, reminderValues); + } + cursor.close(); + } + + private void buildEvent(Long eventId, Calendar component) + throws InvalidComponentException, RemoteException + { + addAttendees(eventId, component); + addReminders(eventId, component); + } + + @Override + public Optional getComponent(Long eventId) + throws RemoteException, InvalidComponentException + { + Cursor cursor = client.query(ContentUris.withAppendedId(getUriForComponents(), eventId), + EventFactory.getProjectionForEvent(), + null, + null, + null); + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + if (cursor.moveToNext()) { + ContentValues eventValues = EventFactory.getValuesForEvent(cursor); + ComponentETagPair component = EventFactory.getEventComponent(getPath(), eventValues); + + buildEvent(eventId, component.getComponent()); + cursor.close(); + return Optional.of(component.getComponent()); + } + + cursor.close(); + return Optional.absent(); + } + + @Override + public Optional> getComponent(String uid) + throws RemoteException, InvalidComponentException + { + String SELECTION = getColumnNameComponentUid() + "=?"; + String[] SELECTION_ARGS = new String[]{uid}; + + Cursor cursor = client.query(getUriForComponents(), + EventFactory.getProjectionForEvent(), + SELECTION, + SELECTION_ARGS, + null); + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + if (cursor.moveToNext()) { + ContentValues eventValues = EventFactory.getValuesForEvent(cursor); + ComponentETagPair component = EventFactory.getEventComponent(getPath(), eventValues); + + buildEvent(eventValues.getAsLong(CalendarContract.Events._ID), component.getComponent()); + cursor.close(); + return Optional.of(component); + } + + cursor.close(); + return Optional.absent(); + } + + @Override + public List> getComponents() + throws RemoteException, InvalidComponentException + { + String SELECTION = getColumnNameCollectionLocalId() + "=?"; + String[] SELECTION_ARGS = new String[]{localId.toString()}; + + List> components = new LinkedList>(); + Cursor cursor = client.query(getUriForComponents(), + EventFactory.getProjectionForEvent(), + SELECTION, + SELECTION_ARGS, null); + + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + while (cursor.moveToNext()) { + ContentValues eventValues = EventFactory.getValuesForEvent(cursor); + Long eventId = eventValues.getAsLong(getColumnNameComponentLocalId()); + + try { + + ComponentETagPair component = EventFactory.getEventComponent(getPath(), eventValues); + buildEvent(eventValues.getAsLong(CalendarContract.Events._ID), component.getComponent()); + components.add(component); + + } catch (InvalidComponentException e) { + if (eventId == null) + throw new InvalidComponentException(e.getMessage(), e.isServersFault(), CalDavConstants.CALDAV_NAMESPACE, getPath(), e); + throw new InvalidLocalComponentException(e.getMessage(), CalDavConstants.CALDAV_NAMESPACE, getPath(), eventId, e); + } + } + + cursor.close(); + return components; + } + + @Override + public void addComponent(ComponentETagPair component) + throws RemoteException, InvalidComponentException + { + ContentValues eventValues = EventFactory.getValuesForEvent(this, localId, component); + int event_op_index = pendingOperations.size(); + + pendingOperations.add(ContentProviderOperation.newInsert(getUriForComponents()) + .withValues(eventValues) + .build()); + + List attendeeValues = EventFactory.getValuesForAttendees(component.getComponent()); + for (ContentValues attendee : attendeeValues) { + pendingOperations.add(ContentProviderOperation.newInsert(getUriForAttendees()) + .withValues(attendee) + .withValueBackReference(CalendarContract.Attendees.EVENT_ID, event_op_index) + .build()); + } + + List reminderValues = EventFactory.getValuesForReminders(component.getComponent()); + for (ContentValues reminder : reminderValues) { + pendingOperations.add(ContentProviderOperation.newInsert(getUriForReminders()) + .withValues(reminder) + .withValueBackReference(CalendarContract.Reminders.EVENT_ID, event_op_index) + .build()); + } + } + + @Override + public void removeComponent(String remoteUId) throws RemoteException { + final String SELECTION = getColumnNameComponentUid() + "=?"; + final String[] SELECTION_ARGS = new String[]{remoteUId}; + final Optional LOCAL_ID = getLocalIdForUid(remoteUId); + + pendingOperations.add(ContentProviderOperation + .newDelete(getUriForComponents()) + .withSelection(SELECTION, SELECTION_ARGS) + .withYieldAllowed(true) + .build()); + + if (LOCAL_ID.isPresent()) { + pendingOperations.add(ContentProviderOperation + .newDelete(ContentUris.withAppendedId(getUriForAttendees(), LOCAL_ID.get())) + .withYieldAllowed(true) + .build()); + + pendingOperations.add(ContentProviderOperation + .newDelete(ContentUris.withAppendedId(getUriForReminders(), LOCAL_ID.get())) + .withYieldAllowed(true) + .build()); + } + } + + @Override + public void updateComponent(ComponentETagPair component) + throws RemoteException, InvalidComponentException + { + try { + + String componentUid = Calendars.getUid(component.getComponent()).getValue(); + + removeComponent(componentUid); + addComponent(component); + + } catch (ConstraintViolationException e) { + Log.d(TAG, "caught exception while updating component ", e); + throw new InvalidComponentException("Caught exception while parsing UID from calendar", false, + CalDavConstants.CALDAV_NAMESPACE, getPath(), e); + } + } + + private boolean hasRecurrenceExceptions(Long eventId) throws RemoteException { + final String[] PROJECTION = new String[]{getColumnNameComponentLocalId(), CalendarContract.Events.ORIGINAL_ID}; + final String SELECTION = CalendarContract.Events.ORIGINAL_ID + "=" + eventId; + + Cursor cursor = client.query(getUriForComponents(), PROJECTION, SELECTION, null, null); + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + if (cursor.moveToNext()) { + cursor.close(); + return true; + } + + cursor.close(); + return false; + } + + private Optional getOriginalIdForRecurrenceException(Long recurrenceExceptionId) + throws RemoteException + { + final String[] PROJECTION = new String[]{CalendarContract.Events.ORIGINAL_ID}; + final String SELECTION = getColumnNameComponentLocalId() + "=" + recurrenceExceptionId; + + Cursor cursor = client.query(getUriForComponents(), PROJECTION, SELECTION, null, null); + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + Optional originalId = Optional.absent(); + if (cursor.moveToNext()) + originalId = Optional.of(cursor.getLong(0)); + + cursor.close(); + return originalId; + } + + private Optional getUidForCopiedEventLocalId(Long copiedEventId) throws RemoteException { + final String[] PROJECTION = new String[]{getColumnNameComponentUid()}; + final String SELECTION = CalendarContract.Events.SYNC_DATA2 + "=" + copiedEventId; + + Cursor cursor = client.query(getUriForComponents(), PROJECTION, SELECTION, null, null); + if (cursor == null) + throw new RemoteException("Content provider client gave us a null cursor!"); + + Optional uid = Optional.absent(); + if (cursor.moveToNext()) + uid = Optional.fromNullable(cursor.getString(0)); + + cursor.close(); + return uid; + } + + private void handleCorrectOrganizersAndAttendees(VEvent vEvent, Account toAccount) + throws InvalidComponentException + { + Uid uid = vEvent.getUid(); + if (uid != null) + uid.setValue(null); + + Organizer oldOrganizer = vEvent.getOrganizer(); + if (oldOrganizer != null) + vEvent.getProperties().remove(oldOrganizer); + + try { + + URI newOrganizerEmail = new URI("mailto", toAccount.name, null); + Organizer newOrganizer = new Organizer(newOrganizerEmail); + vEvent.getProperties().add(newOrganizer); + + PropertyList attendeeList = vEvent.getProperties(Attendee.ATTENDEE); + for (int i = 0; i < attendeeList.size(); i++) { + Attendee attendee = (Attendee) attendeeList.get(i); + if (attendee != null) { + String attendeeEmail = Uri.parse(attendee.getValue()).getSchemeSpecificPart(); + if (attendeeEmail != null && attendeeEmail.equals(account.name)) + attendee.setValue(new URI("mailto", toAccount.name, null).toString()); + } + } + + } catch (URISyntaxException e) { + Log.e(TAG, "caught exception while copying collection to account ", e); + throw new InvalidComponentException("caught exception while copying collection to account", + false, CalDavConstants.CALDAV_NAMESPACE, getPath(), e); + } + } + + private void handleCopyRecurrenceExceptions(Account toAccount, + LocalEventCollection toCollection, + CalendarCopiedListener listener) + throws RemoteException + { + for (Long eventId : getComponentIds()) { + try { + + Optional copyComponent = getComponent(eventId); + if (!copyComponent.isPresent()) + throw new InvalidComponentException("absent returned on copy of " + eventId + " from " + localId, + false, CalDavConstants.CALDAV_NAMESPACE, getPath()); + + VEvent vEvent = (VEvent) copyComponent.get().getComponent(VEvent.VEVENT); + if (vEvent != null) { + + Uid uid = vEvent.getUid(); + if (uid != null) + uid.setValue(null); + + handleCorrectOrganizersAndAttendees(vEvent, toAccount); + + ComponentETagPair correctedComponent = + new ComponentETagPair(copyComponent.get(), Optional.absent()); + + if (EventFactory.isRecurrenceException(vEvent)) { + Log.d(TAG, "found recurrence exception (" + eventId + ") during copy, will copy over now"); + + Optional originalId = getOriginalIdForRecurrenceException(eventId); + if (!originalId.isPresent()) { + throw new InvalidComponentException("could not get original ID for recurrence exception", + false, CalDavConstants.CALDAV_NAMESPACE, getPath()); + } + + Optional parentUid = toCollection.getUidForCopiedEventLocalId(originalId.get()); + if (!parentUid.isPresent()) { + throw new InvalidComponentException("could not get uid for copied event local id", + false, CalDavConstants.CALDAV_NAMESPACE, getPath()); + } + + EventFactory.handleReplaceOriginalSyncId(getPath(), parentUid.get(), vEvent); + toCollection.addComponent(correctedComponent); + toCollection.commitPendingOperations(); + listener.onEventCopied(getAccount(), toAccount, localId); + } + } + else + throw new InvalidComponentException("could not parse VEvent from calendar component.", + false, CalDavConstants.CALDAV_NAMESPACE, getPath()); + + } catch (InvalidComponentException e) { + listener.onEventCopyFailed(e, getAccount(), toAccount, localId); + } catch (RemoteException e) { + listener.onEventCopyFailed(e, getAccount(), toAccount, localId); + } catch (OperationApplicationException e) { + listener.onEventCopyFailed(e, getAccount(), toAccount, localId); + } + } + } + + public void copyToAccount(Account toAccount, + String newCalendarName, + int newCalendarColor, + CalendarCopiedListener listener) + throws RemoteException + { + Log.d(TAG, "copy my " + getComponentIds().size() + + " events to account " + toAccount.name); + + LocalCalendarStore toStore = new LocalCalendarStore(client, toAccount); + String tempRemotePath = UUID.randomUUID().toString(); + Optional toCollection = Optional.absent(); + + try { + + toStore.addCollection(tempRemotePath, newCalendarName, newCalendarColor); + toCollection = toStore.getCollection(tempRemotePath); + + if (!toCollection.isPresent()) { + Log.e(TAG, "local calendar store for " + toAccount.name + + " returned absent for the collection we just copied"); + throw new RemoteException("LocalCalendarStore does not have a copy of our collection!"); + } + + toStore.setCollectionCopied(toCollection.get().getLocalId(), true); + listener.onCalendarCopied(getAccount(), toAccount, localId); + + } catch (RemoteException e) { + listener.onCalendarCopyFailed(e, getAccount(), toAccount, localId); + return; + } + + for (Long eventId : getComponentIds()) { + try { + + Optional copyComponent = getComponent(eventId); + if (!copyComponent.isPresent()) + throw new InvalidComponentException("absent returned on copy of " + eventId + " from " + localId, + false, CalDavConstants.CALDAV_NAMESPACE, getPath()); + + VEvent vEvent = (VEvent) copyComponent.get().getComponent(VEvent.VEVENT); + if (vEvent != null) { + + Uid uid = vEvent.getUid(); + if (uid != null) + uid.setValue(null); + + handleCorrectOrganizersAndAttendees(vEvent, toAccount); + + ComponentETagPair correctedComponent = + new ComponentETagPair(copyComponent.get(), Optional.absent()); + + if (EventFactory.isRecurrenceException(vEvent)) + Log.d(TAG, "found recurrence exception (" + eventId + ") during copy, will copy over next"); + else if (hasRecurrenceExceptions(eventId)) { + EventFactory.handleAttachPropertiesForCopiedRecurrenceWithExceptions(vEvent, eventId); + toCollection.get().addComponent(correctedComponent); + toCollection.get().commitPendingOperations(); + listener.onEventCopied(getAccount(), toAccount, localId); + } + else { + toCollection.get().addComponent(correctedComponent); + toCollection.get().commitPendingOperations(); + listener.onEventCopied(getAccount(), toAccount, localId); + } + } + else + throw new InvalidComponentException("could not parse VEvent from calendar component.", + false, CalDavConstants.CALDAV_NAMESPACE, getPath()); + + } catch (InvalidComponentException e) { + listener.onEventCopyFailed(e, getAccount(), toAccount, localId); + } catch (RemoteException e) { + listener.onEventCopyFailed(e, getAccount(), toAccount, localId); + } catch (OperationApplicationException e) { + listener.onEventCopyFailed(e, getAccount(), toAccount, localId); + } + } + + handleCopyRecurrenceExceptions(toAccount, toCollection.get(), listener); + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/key/KeyProviderStub.java b/flock/src/main/java/org/anhonesteffort/flock/sync/key/KeyProviderStub.java new file mode 100644 index 0000000..63a7016 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/key/KeyProviderStub.java @@ -0,0 +1,62 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.key; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; + +/** + * Programmer: rhodey + */ +public class KeyProviderStub extends ContentProvider { + + @Override + public boolean onCreate() { + return false; + } + + @Override + public Cursor query(Uri uri, String[] strings, String s, String[] strings2, String s2) { + return null; + } + + @Override + public String getType(Uri uri) { + return null; + } + + @Override + public Uri insert(Uri uri, ContentValues contentValues) { + return null; + } + + @Override + public int delete(Uri uri, String s, String[] strings) { + return 0; + } + + @Override + public int update(Uri uri, ContentValues contentValues, String s, String[] strings) { + return 0; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/key/KeySyncScheduler.java b/flock/src/main/java/org/anhonesteffort/flock/sync/key/KeySyncScheduler.java new file mode 100644 index 0000000..839f09c --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/key/KeySyncScheduler.java @@ -0,0 +1,54 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.key; + +import android.content.Context; +import android.net.Uri; + +import org.anhonesteffort.flock.sync.AbstractSyncScheduler; + +/** + * Programmer: rhodey + */ +public class KeySyncScheduler extends AbstractSyncScheduler { + + private static final String TAG = "org.anhonesteffort.flock.sync.key.KeySyncScheduler"; + public static final String CONTENT_AUTHORITY = "org.anhonesteffort.flock.sync.key"; + + public KeySyncScheduler(Context context) { + super(context); + } + + @Override + protected String getTAG() { + return TAG; + } + + @Override + public String getAuthority() { + return CONTENT_AUTHORITY; + } + + @Override + protected Uri getUri() { + return Uri.parse("content://" + CONTENT_AUTHORITY); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/key/KeySyncService.java b/flock/src/main/java/org/anhonesteffort/flock/sync/key/KeySyncService.java new file mode 100644 index 0000000..b5b6896 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/key/KeySyncService.java @@ -0,0 +1,96 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.key; + +import android.accounts.Account; +import android.app.Service; +import android.content.ContentProviderClient; +import android.content.Context; +import android.content.Intent; +import android.content.SyncResult; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.DavAccountHelper; +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.sync.AbstractDavSyncAdapter; +import org.anhonesteffort.flock.sync.calendar.CalendarsSyncScheduler; + +import java.util.Date; + +/** + * Programmer: rhodey + */ +public class KeySyncService extends Service { + + private static final String TAG = "org.anhonesteffort.flock.sync.key.KeySyncService"; + + private static KeySyncAdapter sSyncAdapter = null; + private static final Object sSyncAdapterLock = new Object(); + + @Override + public void onCreate() { + synchronized (sSyncAdapterLock) { + if (sSyncAdapter == null) + sSyncAdapter = new KeySyncAdapter(getApplicationContext()); + } + } + + @Override + public IBinder onBind(Intent intent) { + return sSyncAdapter.getSyncAdapterBinder(); + } + + private static class KeySyncAdapter extends AbstractDavSyncAdapter { + + public KeySyncAdapter(Context context) { + super(context); + } + + protected String getAuthority() { + return KeySyncScheduler.CONTENT_AUTHORITY; + } + + @Override + public void onPerformSync(Account account, + Bundle extras, + String authority, + ContentProviderClient provider, + SyncResult syncResult) + { + Log.d(TAG, "performing sync for authority >> " + authority); + + Optional davAccount = DavAccountHelper.getAccount(getContext()); + if (!davAccount.isPresent()) { + syncResult.stats.numAuthExceptions++; + showNotifications(syncResult); + return; + } + + new KeySyncWorker(getContext(), davAccount.get()).run(syncResult); + + showNotifications(syncResult); + new KeySyncScheduler(getContext()).setTimeLastSync(new Date().getTime()); + } + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/key/KeySyncUtil.java b/flock/src/main/java/org/anhonesteffort/flock/sync/key/KeySyncUtil.java new file mode 100644 index 0000000..8f8bd61 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/key/KeySyncUtil.java @@ -0,0 +1,144 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.key; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.support.v4.app.NotificationCompat; +import android.util.Log; + +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.webdav.caldav.CalDavConstants; + +import org.anhonesteffort.flock.CorrectEncryptionPasswordActivity; +import org.anhonesteffort.flock.DavAccountHelper; +import org.anhonesteffort.flock.R; +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.sync.calendar.HidingCalDavCollection; +import org.anhonesteffort.flock.sync.calendar.HidingCalDavStore; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.jackrabbit.webdav.DavException; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +/** + * Programmer: rhodey + */ +public class KeySyncUtil { + + private static final String TAG = "org.anhonesteffort.flock.sync.key.KeySyncUtil"; + private static final int ID_NOTIFICATION_CIPHER_PASSPHRASE = 1022; + public static final String PATH_KEY_COLLECTION = "key-material/"; + + private static Optional getKeyCollection(Context context, + DavAccount account) + throws PropertyParseException, DavException, IOException + { + HidingCalDavStore store = DavAccountHelper.getHidingCalDavStore(context, account, null); + Optional calendarHomeSet = store.getCalendarHomeSet(); + if (calendarHomeSet.isPresent()) + return store.getCollection(calendarHomeSet.get().concat(PATH_KEY_COLLECTION)); + + return Optional.absent(); + } + + public static HidingCalDavCollection getOrCreateKeyCollection(Context context, + DavAccount account) + throws PropertyParseException, DavException, GeneralSecurityException, IOException + { + Log.d(TAG, "getOrCreateKeyCollection()"); + + Optional keyCollection = getKeyCollection(context, account); + if (keyCollection.isPresent()) + return keyCollection.get(); + + HidingCalDavStore store = DavAccountHelper.getHidingCalDavStore(context, account, null); + Optional calendarHomeSet = store.getCalendarHomeSet(); + + if (!calendarHomeSet.isPresent()) + throw new PropertyParseException("No calendar-home-set property found for user.", + store.getHostHREF(), CalDavConstants.PROPERTY_NAME_CALENDAR_HOME_SET); + + Log.d(TAG, "creating key collection"); + store.addCollection(calendarHomeSet.get().concat(KeySyncUtil.PATH_KEY_COLLECTION)); + keyCollection = store.getCollection(calendarHomeSet.get().concat(KeySyncUtil.PATH_KEY_COLLECTION)); + + if (!keyCollection.isPresent()) + throw new DavException(500, "WebDAV server did not create our key collection!"); + + return keyCollection.get(); + } + + public static Optional getSaltAndEncryptedKeyMaterial(Context context, + DavAccount account) + throws PropertyParseException, DavException, IOException + { + String[] saltAndEncryptedKeyMaterial = {"", ""}; + + Optional keyCollection = getKeyCollection(context, account); + if (keyCollection.isPresent()) { + if (keyCollection.get().getKeyMaterialSalt().isPresent() && + keyCollection.get().getEncryptedKeyMaterial().isPresent()) + { + saltAndEncryptedKeyMaterial[0] = keyCollection.get().getKeyMaterialSalt().get(); + saltAndEncryptedKeyMaterial[1] = keyCollection.get().getEncryptedKeyMaterial().get(); + + keyCollection.get().getStore().closeHttpConnection(); + return Optional.of(saltAndEncryptedKeyMaterial); + } + keyCollection.get().getStore().closeHttpConnection(); + return Optional.absent(); + } + + return Optional.absent(); + } + + public static void showCipherPassphraseInvalidNotification(Context context) { + Log.w(TAG, "showCipherPassphraseInvalidNotification()"); + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context); + + notificationBuilder.setContentTitle(context.getString(R.string.notification_flock_encryption_error)); + notificationBuilder.setContentText(context.getString(R.string.notification_tap_to_correct_encryption_password)); + notificationBuilder.setSmallIcon(R.drawable.alert_warning_light); + notificationBuilder.setAutoCancel(true); + + Intent clickIntent = new Intent(context, CorrectEncryptionPasswordActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity(context, + 0, + clickIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + notificationBuilder.setContentIntent(pendingIntent); + + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(ID_NOTIFICATION_CIPHER_PASSPHRASE, notificationBuilder.build()); + } + + public static void cancelCipherPassphraseNotification(Context context) { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(ID_NOTIFICATION_CIPHER_PASSPHRASE); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/sync/key/KeySyncWorker.java b/flock/src/main/java/org/anhonesteffort/flock/sync/key/KeySyncWorker.java new file mode 100644 index 0000000..a55346c --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/sync/key/KeySyncWorker.java @@ -0,0 +1,137 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.sync.key; + +import android.content.Context; +import android.content.SyncResult; +import android.util.Log; + +import com.google.common.base.Optional; +import org.anhonesteffort.flock.DavAccountHelper; +import org.anhonesteffort.flock.auth.DavAccount; +import org.anhonesteffort.flock.crypto.KeyHelper; +import org.anhonesteffort.flock.crypto.KeyStore; +import org.anhonesteffort.flock.sync.AbstractDavSyncAdapter; +import org.anhonesteffort.flock.sync.calendar.HidingCalDavCollection; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.jackrabbit.webdav.DavException; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +/** + * Programmer: rhodey + */ +public class KeySyncWorker { + + private static final String TAG = "org.anhonesteffort.flock.sync.key.KeySyncWorker"; + + private final Context context; + private final DavAccount account; + + public KeySyncWorker(Context context, DavAccount account) { + this.context = context; + this.account = account; + } + + public void run(SyncResult result) { + Log.d(TAG, "now syncing"); + + Optional localKeyMaterialSalt = Optional.absent(); + Optional localEncryptedKeyMaterial = Optional.absent(); + + try { + + localKeyMaterialSalt = KeyHelper.buildEncodedSalt(context); + localEncryptedKeyMaterial = KeyStore.getEncryptedKeyMaterial(context); + + if (!localKeyMaterialSalt.isPresent() || !localEncryptedKeyMaterial.isPresent()) { + Log.w(TAG, "missing local key material salt or local encrypted key material."); + return; + } + + } catch (IOException e) { + Log.e(TAG, "caught exception while retrieving salt and encrypted key material", e); + AbstractDavSyncAdapter.handleException(context, e, result); + return; + } + + try { + + if (!KeyHelper.masterPassphraseIsValid(context) && + !DavAccountHelper.isUsingOurServers(context)) + { + KeySyncUtil.showCipherPassphraseInvalidNotification(context); + return; + } + + } catch (GeneralSecurityException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + + try { + + HidingCalDavCollection keyCollection = KeySyncUtil.getOrCreateKeyCollection(context, account); + + try { + + Optional remoteKeyMaterialSalt = keyCollection.getKeyMaterialSalt(); + if (!remoteKeyMaterialSalt.isPresent()) { + Log.e(TAG, "remote key material salt is missing, will put local"); + keyCollection.setKeyMaterialSalt(localKeyMaterialSalt.get()); + } + + } catch (PropertyParseException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (DavException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + + Optional remoteKeyMaterial = keyCollection.getEncryptedKeyMaterial(); + if (!remoteKeyMaterial.isPresent()) { + Log.e(TAG, "remote encrypted key material is missing, will put local"); + keyCollection.setEncryptedKeyMaterial(localEncryptedKeyMaterial.get()); + } + + else if (!DavAccountHelper.isUsingOurServers(account) && + !localEncryptedKeyMaterial.get().equals(remoteKeyMaterial.get())) + { + Log.e(TAG, "remote encrypted key material is different, will import locally"); + KeyStore.saveEncryptedKeyMaterial(context, remoteKeyMaterial.get()); + KeySyncUtil.showCipherPassphraseInvalidNotification(context); + } + + keyCollection.getStore().closeHttpConnection(); + + } catch (PropertyParseException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (DavException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (GeneralSecurityException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } catch (IOException e) { + AbstractDavSyncAdapter.handleException(context, e, result); + } + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/util/Base64.java b/flock/src/main/java/org/anhonesteffort/flock/util/Base64.java new file mode 100644 index 0000000..13705e0 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/util/Base64.java @@ -0,0 +1,2116 @@ +/* + * * + * Copyright (C) 2014 Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.util; + +import java.io.IOException; + +/** + *

Encodes and decodes to and from Base64 notation.

+ *

Homepage: http://iharder.net/base64.

+ * + *

Example:

+ * + * String encoded = Base64.encode( myByteArray ); + *
+ * byte[] myByteArray = Base64.decode( encoded ); + * + *

The options parameter, which appears in a few places, is used to pass + * several pieces of information to the encoder. In the "higher level" methods such as + * encodeBytes( bytes, options ) the options parameter can be used to indicate such + * things as first gzipping the bytes before encoding them, not inserting linefeeds, + * and encoding using the URL-safe and Ordered dialects.

+ * + *

Note, according to RFC3548, + * Section 2.1, implementations should not add line feeds unless explicitly told + * to do so. I've got Base64 set to this behavior now, although earlier versions + * broke lines by default.

+ * + *

The constants defined in Base64 can be OR-ed together to combine options, so you + * might make a call like this:

+ * + * String encoded = Base64.encodeBytes( mybytes, Base64.GZIP | Base64.DO_BREAK_LINES ); + *

to compress the data before encoding it and then making the output have newline characters.

+ *

Also...

+ * String encoded = Base64.encodeBytes( crazyString.getBytes() ); + * + * + * + *

+ * Change Log: + *

+ *
    + *
  • v2.3.4 - Fixed bug when working with gzipped streams whereby flushing + * the Base64.OutputStream closed the Base64 encoding (by padding with equals + * signs) too soon. Also added an option to suppress the automatic decoding + * of gzipped streams. Also added experimental support for specifying a + * class loader when using the + * {@link #decodeToObject(String, int, ClassLoader)} + * method.
  • + *
  • v2.3.3 - Changed default char encoding to US-ASCII which reduces the internal Java + * footprint with its CharEncoders and so forth. Fixed some javadocs that were + * inconsistent. Removed imports and specified things like java.io.IOException + * explicitly inline.
  • + *
  • v2.3.2 - Reduced memory footprint! Finally refined the "guessing" of how big the + * final encoded data will be so that the code doesn't have to create two output + * arrays: an oversized initial one and then a final, exact-sized one. Big win + * when using the {@link #encodeBytesToBytes(byte[])} family of methods (and not + * using the gzip options which uses a different mechanism with streams and stuff).
  • + *
  • v2.3.1 - Added {@link #encodeBytesToBytes(byte[], int, int, int)} and some + * similar helper methods to be more efficient with memory by not returning a + * String but just a byte array.
  • + *
  • v2.3 - This is not a drop-in replacement! This is two years of comments + * and bug fixes queued up and finally executed. Thanks to everyone who sent + * me stuff, and I'm sorry I wasn't able to distribute your fixes to everyone else. + * Much bad coding was cleaned up including throwing exceptions where necessary + * instead of returning null values or something similar. Here are some changes + * that may affect you: + *
      + *
    • Does not break lines, by default. This is to keep in compliance with + * RFC3548.
    • + *
    • Throws exceptions instead of returning null values. Because some operations + * (especially those that may permit the GZIP option) use IO streams, there + * is a possiblity of an java.io.IOException being thrown. After some discussion and + * thought, I've changed the behavior of the methods to throw java.io.IOExceptions + * rather than return null if ever there's an error. I think this is more + * appropriate, though it will require some changes to your code. Sorry, + * it should have been done this way to begin with.
    • + *
    • Removed all references to System.out, System.err, and the like. + * Shame on me. All I can say is sorry they were ever there.
    • + *
    • Throws NullPointerExceptions and IllegalArgumentExceptions as needed + * such as when passed arrays are null or offsets are invalid.
    • + *
    • Cleaned up as much javadoc as I could to avoid any javadoc warnings. + * This was especially annoying before for people who were thorough in their + * own projects and then had gobs of javadoc warnings on this file.
    • + *
    + *
  • v2.2.1 - Fixed bug using URL_SAFE and ORDERED encodings. Fixed bug + * when using very small files (~< 40 bytes).
  • + *
  • v2.2 - Added some helper methods for encoding/decoding directly from + * one file to the next. Also added a main() method to support command line + * encoding/decoding from one file to the next. Also added these Base64 dialects: + *
      + *
    1. The default is RFC3548 format.
    2. + *
    3. Calling Base64.setFormat(Base64.BASE64_FORMAT.URLSAFE_FORMAT) generates + * URL and file name friendly format as described in Section 4 of RFC3548. + * http://www.faqs.org/rfcs/rfc3548.html
    4. + *
    5. Calling Base64.setFormat(Base64.BASE64_FORMAT.ORDERED_FORMAT) generates + * URL and file name friendly format that preserves lexical ordering as described + * in http://www.faqs.org/qa/rfcc-1940.html
    6. + *
    + * Special thanks to Jim Kellerman at http://www.powerset.com/ + * for contributing the new Base64 dialects. + *
  • + * + *
  • v2.1 - Cleaned up javadoc comments and unused variables and methods. Added + * some convenience methods for reading and writing to and from files.
  • + *
  • v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems + * with other encodings (like EBCDIC).
  • + *
  • v2.0.1 - Fixed an error when decoding a single byte, that is, when the + * encoded data was a single byte.
  • + *
  • v2.0 - I got rid of methods that used booleans to set options. + * Now everything is more consolidated and cleaner. The code now detects + * when data that's being decoded is gzip-compressed and will decompress it + * automatically. Generally things are cleaner. You'll probably have to + * change some method calls that you were making to support the new + * options format (ints that you "OR" together).
  • + *
  • v1.5.1 - Fixed bug when decompressing and decoding to a + * byte[] using decode( String s, boolean gzipCompressed ). + * Added the ability to "suspend" encoding in the Output Stream so + * you can turn on and off the encoding if you need to embed base64 + * data in an otherwise "normal" stream (like an XML file).
  • + *
  • v1.5 - Output stream pases on flush() command but doesn't do anything itself. + * This helps when using GZIP streams. + * Added the ability to GZip-compress objects before encoding them.
  • + *
  • v1.4 - Added helper methods to read/write files.
  • + *
  • v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.
  • + *
  • v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream + * where last buffer being read, if not completely full, was not returned.
  • + *
  • v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.
  • + *
  • v1.3.3 - Fixed I/O streams which were totally messed up.
  • + *
+ * + *

+ * I am placing this code in the Public Domain. Do with it as you will. + * This software comes with no guarantees or warranties but with + * plenty of well-wishing instead! + * Please visit http://iharder.net/base64 + * periodically to check for updates or to contribute improvements. + *

+ * + * @author Robert Harder + * @author rob@iharder.net + * @version 2.3.3 + */ +public class Base64 +{ + +/* ******** P U B L I C F I E L D S ******** */ + + + /** No options specified. Value is zero. */ + public final static int NO_OPTIONS = 0; + + /** Specify encoding in first bit. Value is one. */ + public final static int ENCODE = 1; + + + /** Specify decoding in first bit. Value is zero. */ + public final static int DECODE = 0; + + + /** Specify that data should be gzip-compressed in second bit. Value is two. */ + public final static int GZIP = 2; + + /** Specify that gzipped data should not be automatically gunzipped. */ + public final static int DONT_GUNZIP = 4; + + + /** Do break lines when encoding. Value is 8. */ + public final static int DO_BREAK_LINES = 8; + + /** + * Encode using Base64-like encoding that is URL- and Filename-safe as described + * in Section 4 of RFC3548: + * http://www.faqs.org/rfcs/rfc3548.html. + * It is important to note that data encoded this way is not officially valid Base64, + * or at the very least should not be called Base64 without also specifying that is + * was encoded using the URL- and Filename-safe dialect. + */ + public final static int URL_SAFE = 16; + + + /** + * Encode using the special "ordered" dialect of Base64 described here: + * http://www.faqs.org/qa/rfcc-1940.html. + */ + public final static int ORDERED = 32; + + +/* ******** P R I V A T E F I E L D S ******** */ + + + /** Maximum line length (76) of Base64 output. */ + private final static int MAX_LINE_LENGTH = 76; + + + /** The equals sign (=) as a byte. */ + private final static byte EQUALS_SIGN = (byte)'='; + + + /** The new line character (\n) as a byte. */ + private final static byte NEW_LINE = (byte)'\n'; + + + /** Preferred encoding. */ + private final static String PREFERRED_ENCODING = "US-ASCII"; + + + private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding + private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding + + +/* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */ + + /** The 64 valid Base64 values. */ + /* Host platform me be something funny like EBCDIC, so we hardcode these values. */ + private final static byte[] _STANDARD_ALPHABET = { + (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', + (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', + (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', + (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', + (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', + (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', + (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', + (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', + (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', + (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/' + }; + + + /** + * Translates a Base64 value to either its 6-bit reconstruction value + * or a negative number indicating some other meaning. + **/ + private final static byte[] _STANDARD_DECODABET = { + -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 + -5,-5, // Whitespace: Tab and Linefeed + -9,-9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 + -9,-9,-9,-9,-9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 + 62, // Plus sign at decimal 43 + -9,-9,-9, // Decimal 44 - 46 + 63, // Slash at decimal 47 + 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine + -9,-9,-9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9,-9,-9, // Decimal 62 - 64 + 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N' + 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z' + -9,-9,-9,-9,-9,-9, // Decimal 91 - 96 + 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm' + 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z' + -9,-9,-9,-9 // Decimal 123 - 126 + /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + }; + + +/* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */ + + /** + * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548: + * http://www.faqs.org/rfcs/rfc3548.html. + * Notice that the last two bytes become "hyphen" and "underscore" instead of "plus" and "slash." + */ + private final static byte[] _URL_SAFE_ALPHABET = { + (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', + (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', + (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', + (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', + (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', + (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', + (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', + (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', + (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', + (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'-', (byte)'_' + }; + + /** + * Used in decoding URL- and Filename-safe dialects of Base64. + */ + private final static byte[] _URL_SAFE_DECODABET = { + -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 + -5,-5, // Whitespace: Tab and Linefeed + -9,-9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 + -9,-9,-9,-9,-9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 + -9, // Plus sign at decimal 43 + -9, // Decimal 44 + 62, // Minus sign at decimal 45 + -9, // Decimal 46 + -9, // Slash at decimal 47 + 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine + -9,-9,-9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9,-9,-9, // Decimal 62 - 64 + 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N' + 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z' + -9,-9,-9,-9, // Decimal 91 - 94 + 63, // Underscore at decimal 95 + -9, // Decimal 96 + 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm' + 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z' + -9,-9,-9,-9 // Decimal 123 - 126 + /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + }; + + + +/* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */ + + /** + * I don't get the point of this technique, but someone requested it, + * and it is described here: + * http://www.faqs.org/qa/rfcc-1940.html. + */ + private final static byte[] _ORDERED_ALPHABET = { + (byte)'-', + (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', + (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', + (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', + (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', + (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', + (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', + (byte)'_', + (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', + (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', + (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', + (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z' + }; + + /** + * Used in decoding the "ordered" dialect of Base64. + */ + private final static byte[] _ORDERED_DECODABET = { + -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 + -5,-5, // Whitespace: Tab and Linefeed + -9,-9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 + -9,-9,-9,-9,-9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 + -9, // Plus sign at decimal 43 + -9, // Decimal 44 + 0, // Minus sign at decimal 45 + -9, // Decimal 46 + -9, // Slash at decimal 47 + 1,2,3,4,5,6,7,8,9,10, // Numbers zero through nine + -9,-9,-9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9,-9,-9, // Decimal 62 - 64 + 11,12,13,14,15,16,17,18,19,20,21,22,23, // Letters 'A' through 'M' + 24,25,26,27,28,29,30,31,32,33,34,35,36, // Letters 'N' through 'Z' + -9,-9,-9,-9, // Decimal 91 - 94 + 37, // Underscore at decimal 95 + -9, // Decimal 96 + 38,39,40,41,42,43,44,45,46,47,48,49,50, // Letters 'a' through 'm' + 51,52,53,54,55,56,57,58,59,60,61,62,63, // Letters 'n' through 'z' + -9,-9,-9,-9 // Decimal 123 - 126 + /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + }; + + +/* ******** D E T E R M I N E W H I C H A L H A B E T ******** */ + + + /** + * Returns one of the _SOMETHING_ALPHABET byte arrays depending on + * the options specified. + * It's possible, though silly, to specify ORDERED and URLSAFE + * in which case one of them will be picked, though there is + * no guarantee as to which one will be picked. + */ + private final static byte[] getAlphabet( int options ) { + if ((options & URL_SAFE) == URL_SAFE) { + return _URL_SAFE_ALPHABET; + } else if ((options & ORDERED) == ORDERED) { + return _ORDERED_ALPHABET; + } else { + return _STANDARD_ALPHABET; + } + } // end getAlphabet + + + /** + * Returns one of the _SOMETHING_DECODABET byte arrays depending on + * the options specified. + * It's possible, though silly, to specify ORDERED and URL_SAFE + * in which case one of them will be picked, though there is + * no guarantee as to which one will be picked. + */ + private final static byte[] getDecodabet( int options ) { + if( (options & URL_SAFE) == URL_SAFE) { + return _URL_SAFE_DECODABET; + } else if ((options & ORDERED) == ORDERED) { + return _ORDERED_DECODABET; + } else { + return _STANDARD_DECODABET; + } + } // end getAlphabet + + + + /** Defeats instantiation. */ + private Base64(){} + + + + public static int getEncodedLengthWithoutPadding(int unencodedLength) { + int remainderBytes = unencodedLength % 3; + int paddingBytes = 0; + + if (remainderBytes != 0) + paddingBytes = 3 - remainderBytes; + + return (((int)((unencodedLength+2)/3))*4) - paddingBytes; + } + + public static int getEncodedBytesForTarget(int targetSize) { + return ((int)(targetSize * 3)) / 4; + } + + +/* ******** E N C O D I N G M E T H O D S ******** */ + + + /** + * Encodes up to the first three bytes of array threeBytes + * and returns a four-byte array in Base64 notation. + * The actual number of significant bytes in your array is + * given by numSigBytes. + * The array threeBytes needs only be as big as + * numSigBytes. + * Code can reuse a byte array by passing a four-byte array as b4. + * + * @param b4 A reusable byte array to reduce array instantiation + * @param threeBytes the array to convert + * @param numSigBytes the number of significant bytes in your array + * @return four byte array in Base64 notation. + * @since 1.5.1 + */ + private static byte[] encode3to4( byte[] b4, byte[] threeBytes, int numSigBytes, int options ) { + encode3to4( threeBytes, 0, numSigBytes, b4, 0, options ); + return b4; + } // end encode3to4 + + + /** + *

Encodes up to three bytes of the array source + * and writes the resulting four Base64 bytes to destination. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * srcOffset and destOffset. + * This method does not check to make sure your arrays + * are large enough to accomodate srcOffset + 3 for + * the source array or destOffset + 4 for + * the destination array. + * The actual number of significant bytes in your array is + * given by numSigBytes.

+ *

This is the lowest level of the encoding methods with + * all possible parameters.

+ * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param numSigBytes the number of significant bytes in your array + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @return the destination array + * @since 1.3 + */ + private static byte[] encode3to4( + byte[] source, int srcOffset, int numSigBytes, + byte[] destination, int destOffset, int options ) { + + byte[] ALPHABET = getAlphabet( options ); + + // 1 2 3 + // 01234567890123456789012345678901 Bit position + // --------000000001111111122222222 Array position from threeBytes + // --------| || || || | Six bit groups to index ALPHABET + // >>18 >>12 >> 6 >> 0 Right shift necessary + // 0x3f 0x3f 0x3f Additional AND + + // Create buffer with zero-padding if there are only one or two + // significant bytes passed in the array. + // We have to shift left 24 in order to flush out the 1's that appear + // when Java treats a value as negative that is cast from a byte to an int. + int inBuff = ( numSigBytes > 0 ? ((source[ srcOffset ] << 24) >>> 8) : 0 ) + | ( numSigBytes > 1 ? ((source[ srcOffset + 1 ] << 24) >>> 16) : 0 ) + | ( numSigBytes > 2 ? ((source[ srcOffset + 2 ] << 24) >>> 24) : 0 ); + + switch( numSigBytes ) + { + case 3: + destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; + destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; + destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ]; + destination[ destOffset + 3 ] = ALPHABET[ (inBuff ) & 0x3f ]; + return destination; + + case 2: + destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; + destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; + destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ]; + destination[ destOffset + 3 ] = EQUALS_SIGN; + return destination; + + case 1: + destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; + destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; + destination[ destOffset + 2 ] = EQUALS_SIGN; + destination[ destOffset + 3 ] = EQUALS_SIGN; + return destination; + + default: + return destination; + } // end switch + } // end encode3to4 + + + + /** + * Performs Base64 encoding on the raw ByteBuffer, + * writing it to the encoded ByteBuffer. + * This is an experimental feature. Currently it does not + * pass along any options (such as {@link #DO_BREAK_LINES} + * or {@link #GZIP}. + * + * @param raw input buffer + * @param encoded output buffer + * @since 2.3 + */ + public static void encode( java.nio.ByteBuffer raw, java.nio.ByteBuffer encoded ){ + byte[] raw3 = new byte[3]; + byte[] enc4 = new byte[4]; + + while( raw.hasRemaining() ){ + int rem = Math.min(3,raw.remaining()); + raw.get(raw3,0,rem); + Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS ); + encoded.put(enc4); + } // end input remaining + } + + + /** + * Performs Base64 encoding on the raw ByteBuffer, + * writing it to the encoded CharBuffer. + * This is an experimental feature. Currently it does not + * pass along any options (such as {@link #DO_BREAK_LINES} + * or {@link #GZIP}. + * + * @param raw input buffer + * @param encoded output buffer + * @since 2.3 + */ + public static void encode( java.nio.ByteBuffer raw, java.nio.CharBuffer encoded ){ + byte[] raw3 = new byte[3]; + byte[] enc4 = new byte[4]; + + while( raw.hasRemaining() ){ + int rem = Math.min(3,raw.remaining()); + raw.get(raw3,0,rem); + Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS ); + for( int i = 0; i < 4; i++ ){ + encoded.put( (char)(enc4[i] & 0xFF) ); + } + } // end input remaining + } + + + + + /** + * Serializes an object and returns the Base64-encoded + * version of that serialized object. + * + *

As of v 2.3, if the object + * cannot be serialized or there is another error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.

+ * + * The object is not GZip-compressed before being encoded. + * + * @param serializableObject The object to encode + * @return The Base64-encoded object + * @throws java.io.IOException if there is an error + * @throws NullPointerException if serializedObject is null + * @since 1.4 + */ + public static String encodeObject( java.io.Serializable serializableObject ) + throws IOException { + return encodeObject( serializableObject, NO_OPTIONS ); + } // end encodeObject + + + + /** + * Serializes an object and returns the Base64-encoded + * version of that serialized object. + * + *

As of v 2.3, if the object + * cannot be serialized or there is another error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.

+ * + * The object is not GZip-compressed before being encoded. + *

+ * Example options:

+     *   GZIP: gzip-compresses object before encoding it.
+     *   DO_BREAK_LINES: break lines at 76 characters
+     * 
+ *

+ * Example: encodeObject( myObj, Base64.GZIP ) or + *

+ * Example: encodeObject( myObj, Base64.GZIP | Base64.DO_BREAK_LINES ) + * + * @param serializableObject The object to encode + * @param options Specified options + * @return The Base64-encoded object + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @throws java.io.IOException if there is an error + * @since 2.0 + */ + public static String encodeObject( java.io.Serializable serializableObject, int options ) + throws IOException { + + if( serializableObject == null ){ + throw new NullPointerException( "Cannot serialize a null object." ); + } // end if: null + + // Streams + java.io.ByteArrayOutputStream baos = null; + java.io.OutputStream b64os = null; + java.util.zip.GZIPOutputStream gzos = null; + java.io.ObjectOutputStream oos = null; + + + try { + // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream + baos = new java.io.ByteArrayOutputStream(); + b64os = new Base64.OutputStream( baos, ENCODE | options ); + if( (options & GZIP) != 0 ){ + // Gzip + gzos = new java.util.zip.GZIPOutputStream(b64os); + oos = new java.io.ObjectOutputStream( gzos ); + } else { + // Not gzipped + oos = new java.io.ObjectOutputStream( b64os ); + } + oos.writeObject( serializableObject ); + } // end try + catch( IOException e ) { + // Catch it and then throw it immediately so that + // the finally{} block is called for cleanup. + throw e; + } // end catch + finally { + try{ oos.close(); } catch( Exception e ){} + try{ gzos.close(); } catch( Exception e ){} + try{ b64os.close(); } catch( Exception e ){} + try{ baos.close(); } catch( Exception e ){} + } // end finally + + // Return value according to relevant encoding. + try { + return new String( baos.toByteArray(), PREFERRED_ENCODING ); + } // end try + catch (java.io.UnsupportedEncodingException uue){ + // Fall back to some Java default + return new String( baos.toByteArray() ); + } // end catch + + } // end encode + + + + /** + * Encodes a byte array into Base64 notation. + * Does not GZip-compress data. + * + * @param source The data to convert + * @return The data in Base64-encoded form + * @throws NullPointerException if source array is null + * @since 1.4 + */ + public static String encodeBytes( byte[] source ) { + // Since we're not going to have the GZIP encoding turned on, + // we're not going to have an java.io.IOException thrown, so + // we should not force the user to have to catch it. + String encoded = null; + try { + encoded = encodeBytes(source, 0, source.length, NO_OPTIONS); + } catch (IOException ex) { + assert false : ex.getMessage(); + } // end catch + assert encoded != null; + return encoded; + } // end encodeBytes + + + public static String encodeBytesWithoutPadding(byte[] source, int offset, int length) { + String encoded = null; + + try { + encoded = encodeBytes(source, offset, length, NO_OPTIONS); + } catch (IOException ex) { + assert false : ex.getMessage(); + } + + assert encoded != null; + + if (encoded.charAt(encoded.length()-2) == '=') return encoded.substring(0, encoded.length()-2); + else if (encoded.charAt(encoded.length()-1) == '=') return encoded.substring(0, encoded.length()-1); + else return encoded; + + } + + public static String encodeBytesWithoutPadding(byte[] source) { + return encodeBytesWithoutPadding(source, 0, source.length); + } + + + /** + * Encodes a byte array into Base64 notation. + *

+ * Example options:

+     *   GZIP: gzip-compresses object before encoding it.
+     *   DO_BREAK_LINES: break lines at 76 characters
+     *     Note: Technically, this makes your encoding non-compliant.
+     * 
+ *

+ * Example: encodeBytes( myData, Base64.GZIP ) or + *

+ * Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES ) + * + * + *

As of v 2.3, if there is an error with the GZIP stream, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.

+ * + * + * @param source The data to convert + * @param options Specified options + * @return The Base64-encoded data as a String + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @throws java.io.IOException if there is an error + * @throws NullPointerException if source array is null + * @since 2.0 + */ + public static String encodeBytes( byte[] source, int options ) throws IOException { + return encodeBytes( source, 0, source.length, options ); + } // end encodeBytes + + + /** + * Encodes a byte array into Base64 notation. + * Does not GZip-compress data. + * + *

As of v 2.3, if there is an error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.

+ * + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @return The Base64-encoded data as a String + * @throws NullPointerException if source array is null + * @throws IllegalArgumentException if source array, offset, or length are invalid + * @since 1.4 + */ + public static String encodeBytes( byte[] source, int off, int len ) { + // Since we're not going to have the GZIP encoding turned on, + // we're not going to have an java.io.IOException thrown, so + // we should not force the user to have to catch it. + String encoded = null; + try { + encoded = encodeBytes( source, off, len, NO_OPTIONS ); + } catch (IOException ex) { + assert false : ex.getMessage(); + } // end catch + assert encoded != null; + return encoded; + } // end encodeBytes + + + + /** + * Encodes a byte array into Base64 notation. + *

+ * Example options:

+     *   GZIP: gzip-compresses object before encoding it.
+     *   DO_BREAK_LINES: break lines at 76 characters
+     *     Note: Technically, this makes your encoding non-compliant.
+     * 
+ *

+ * Example: encodeBytes( myData, Base64.GZIP ) or + *

+ * Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES ) + * + * + *

As of v 2.3, if there is an error with the GZIP stream, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.

+ * + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param options Specified options + * @return The Base64-encoded data as a String + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @throws java.io.IOException if there is an error + * @throws NullPointerException if source array is null + * @throws IllegalArgumentException if source array, offset, or length are invalid + * @since 2.0 + */ + public static String encodeBytes( byte[] source, int off, int len, int options ) throws IOException { + byte[] encoded = encodeBytesToBytes( source, off, len, options ); + + // Return value according to relevant encoding. + try { + return new String( encoded, PREFERRED_ENCODING ); + } // end try + catch (java.io.UnsupportedEncodingException uue) { + return new String( encoded ); + } // end catch + + } // end encodeBytes + + + + + /** + * Similar to {@link #encodeBytes(byte[])} but returns + * a byte array instead of instantiating a String. This is more efficient + * if you're working with I/O streams and have large data sets to encode. + * + * + * @param source The data to convert + * @return The Base64-encoded data as a byte[] (of ASCII characters) + * @throws NullPointerException if source array is null + * @since 2.3.1 + */ + public static byte[] encodeBytesToBytes( byte[] source ) { + byte[] encoded = null; + try { + encoded = encodeBytesToBytes( source, 0, source.length, Base64.NO_OPTIONS ); + } catch( IOException ex ) { + assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage(); + } + return encoded; + } + + + /** + * Similar to {@link #encodeBytes(byte[], int, int, int)} but returns + * a byte array instead of instantiating a String. This is more efficient + * if you're working with I/O streams and have large data sets to encode. + * + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param options Specified options + * @return The Base64-encoded data as a String + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @throws java.io.IOException if there is an error + * @throws NullPointerException if source array is null + * @throws IllegalArgumentException if source array, offset, or length are invalid + * @since 2.3.1 + */ + public static byte[] encodeBytesToBytes( byte[] source, int off, int len, int options ) throws IOException { + + if( source == null ){ + throw new NullPointerException( "Cannot serialize a null array." ); + } // end if: null + + if( off < 0 ){ + throw new IllegalArgumentException( "Cannot have negative offset: " + off ); + } // end if: off < 0 + + if( len < 0 ){ + throw new IllegalArgumentException( "Cannot have length offset: " + len ); + } // end if: len < 0 + + if( off + len > source.length ){ + throw new IllegalArgumentException( + String.format( "Cannot have offset of %d and length of %d with array of length %d", off,len,source.length)); + } // end if: off < 0 + + + + // Compress? + if( (options & GZIP) != 0 ) { + java.io.ByteArrayOutputStream baos = null; + java.util.zip.GZIPOutputStream gzos = null; + Base64.OutputStream b64os = null; + + try { + // GZip -> Base64 -> ByteArray + baos = new java.io.ByteArrayOutputStream(); + b64os = new Base64.OutputStream( baos, ENCODE | options ); + gzos = new java.util.zip.GZIPOutputStream( b64os ); + + gzos.write( source, off, len ); + gzos.close(); + } // end try + catch( IOException e ) { + // Catch it and then throw it immediately so that + // the finally{} block is called for cleanup. + throw e; + } // end catch + finally { + try{ gzos.close(); } catch( Exception e ){} + try{ b64os.close(); } catch( Exception e ){} + try{ baos.close(); } catch( Exception e ){} + } // end finally + + return baos.toByteArray(); + } // end if: compress + + // Else, don't compress. Better not to use streams at all then. + else { + boolean breakLines = (options & DO_BREAK_LINES) > 0; + + //int len43 = len * 4 / 3; + //byte[] outBuff = new byte[ ( len43 ) // Main 4:3 + // + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding + // + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines + // Try to determine more precisely how big the array needs to be. + // If we get it right, we don't have to do an array copy, and + // we save a bunch of memory. + int encLen = ( len / 3 ) * 4 + ( len % 3 > 0 ? 4 : 0 ); // Bytes needed for actual encoding + if( breakLines ){ + encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline characters + } + byte[] outBuff = new byte[ encLen ]; + + + int d = 0; + int e = 0; + int len2 = len - 2; + int lineLength = 0; + for( ; d < len2; d+=3, e+=4 ) { + encode3to4( source, d+off, 3, outBuff, e, options ); + + lineLength += 4; + if( breakLines && lineLength >= MAX_LINE_LENGTH ) + { + outBuff[e+4] = NEW_LINE; + e++; + lineLength = 0; + } // end if: end of line + } // en dfor: each piece of array + + if( d < len ) { + encode3to4( source, d+off, len - d, outBuff, e, options ); + e += 4; + } // end if: some padding needed + + + // Only resize array if we didn't guess it right. + if( e < outBuff.length - 1 ){ + byte[] finalOut = new byte[e]; + System.arraycopy(outBuff,0, finalOut,0,e); + //System.err.println("Having to resize array from " + outBuff.length + " to " + e ); + return finalOut; + } else { + //System.err.println("No need to resize array."); + return outBuff; + } + + } // end else: don't compress + + } // end encodeBytesToBytes + + + + + +/* ******** D E C O D I N G M E T H O D S ******** */ + + + /** + * Decodes four bytes from array source + * and writes the resulting bytes (up to three of them) + * to destination. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * srcOffset and destOffset. + * This method does not check to make sure your arrays + * are large enough to accomodate srcOffset + 4 for + * the source array or destOffset + 3 for + * the destination array. + * This method returns the actual number of bytes that + * were converted from the Base64 encoding. + *

This is the lowest level of the decoding methods with + * all possible parameters.

+ * + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @param options alphabet type is pulled from this (standard, url-safe, ordered) + * @return the number of decoded bytes converted + * @throws NullPointerException if source or destination arrays are null + * @throws IllegalArgumentException if srcOffset or destOffset are invalid + * or there is not enough room in the array. + * @since 1.3 + */ + private static int decode4to3( + byte[] source, int srcOffset, + byte[] destination, int destOffset, int options ) { + + // Lots of error checking and exception throwing + if( source == null ){ + throw new NullPointerException( "Source array was null." ); + } // end if + if( destination == null ){ + throw new NullPointerException( "Destination array was null." ); + } // end if + if( srcOffset < 0 || srcOffset + 3 >= source.length ){ + throw new IllegalArgumentException( String.format( + "Source array with length %d cannot have offset of %d and still process four bytes.", source.length, srcOffset ) ); + } // end if + if( destOffset < 0 || destOffset +2 >= destination.length ){ + throw new IllegalArgumentException( String.format( + "Destination array with length %d cannot have offset of %d and still store three bytes.", destination.length, destOffset ) ); + } // end if + + + byte[] DECODABET = getDecodabet( options ); + + // Example: Dk== + if( source[ srcOffset + 2] == EQUALS_SIGN ) { + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); + int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) + | ( ( DECODABET[ source[ srcOffset + 1] ] & 0xFF ) << 12 ); + + destination[ destOffset ] = (byte)( outBuff >>> 16 ); + return 1; + } + + // Example: DkL= + else if( source[ srcOffset + 3 ] == EQUALS_SIGN ) { + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); + int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) + | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 ) + | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6 ); + + destination[ destOffset ] = (byte)( outBuff >>> 16 ); + destination[ destOffset + 1 ] = (byte)( outBuff >>> 8 ); + return 2; + } + + // Example: DkLE + else { + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) + // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); + int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) + | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 ) + | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6) + | ( ( DECODABET[ source[ srcOffset + 3 ] ] & 0xFF ) ); + + + destination[ destOffset ] = (byte)( outBuff >> 16 ); + destination[ destOffset + 1 ] = (byte)( outBuff >> 8 ); + destination[ destOffset + 2 ] = (byte)( outBuff ); + + return 3; + } + } // end decodeToBytes + + + + + + /** + * Low-level access to decoding ASCII characters in + * the form of a byte array. Ignores GUNZIP option, if + * it's set. This is not generally a recommended method, + * although it is used internally as part of the decoding process. + * Special case: if len = 0, an empty array is returned. Still, + * if you need more speed and reduced memory footprint (and aren't + * gzipping), consider this method. + * + * @param source The Base64 encoded data + * @return decoded data + * @since 2.3.1 + */ + public static byte[] decode( byte[] source ){ + byte[] decoded = null; + try { + decoded = decode( source, 0, source.length, Base64.NO_OPTIONS ); + } catch( IOException ex ) { + assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage(); + } + return decoded; + } + + + /** + * Low-level access to decoding ASCII characters in + * the form of a byte array. Ignores GUNZIP option, if + * it's set. This is not generally a recommended method, + * although it is used internally as part of the decoding process. + * Special case: if len = 0, an empty array is returned. Still, + * if you need more speed and reduced memory footprint (and aren't + * gzipping), consider this method. + * + * @param source The Base64 encoded data + * @param off The offset of where to begin decoding + * @param len The length of characters to decode + * @param options Can specify options such as alphabet type to use + * @return decoded data + * @throws java.io.IOException If bogus characters exist in source data + * @since 1.3 + */ + public static byte[] decode( byte[] source, int off, int len, int options ) + throws IOException { + + // Lots of error checking and exception throwing + if( source == null ){ + throw new NullPointerException( "Cannot decode null source array." ); + } // end if + if( off < 0 || off + len > source.length ){ + throw new IllegalArgumentException( String.format( + "Source array with length %d cannot have offset of %d and process %d bytes.", source.length, off, len ) ); + } // end if + + if( len == 0 ){ + return new byte[0]; + }else if( len < 4 ){ + throw new IllegalArgumentException( + "Base64-encoded string must have at least four characters, but length specified was " + len ); + } // end if + + byte[] DECODABET = getDecodabet( options ); + + int len34 = len * 3 / 4; // Estimate on array size + byte[] outBuff = new byte[ len34 ]; // Upper limit on size of output + int outBuffPosn = 0; // Keep track of where we're writing + + byte[] b4 = new byte[4]; // Four byte buffer from source, eliminating white space + int b4Posn = 0; // Keep track of four byte input buffer + int i = 0; // Source array counter + byte sbiCrop = 0; // Low seven bits (ASCII) of input + byte sbiDecode = 0; // Special value from DECODABET + + for( i = off; i < off+len; i++ ) { // Loop through source + + sbiCrop = (byte)(source[i] & 0x7f); // Only the low seven bits + sbiDecode = DECODABET[ sbiCrop ]; // Special value + + // White space, Equals sign, or legit Base64 character + // Note the values such as -5 and -9 in the + // DECODABETs at the top of the file. + if( sbiDecode >= WHITE_SPACE_ENC ) { + if( sbiDecode >= EQUALS_SIGN_ENC ) { + b4[ b4Posn++ ] = sbiCrop; // Save non-whitespace + if( b4Posn > 3 ) { // Time to decode? + outBuffPosn += decode4to3( b4, 0, outBuff, outBuffPosn, options ); + b4Posn = 0; + + // If that was the equals sign, break out of 'for' loop + if( sbiCrop == EQUALS_SIGN ) { + break; + } // end if: equals sign + } // end if: quartet built + } // end if: equals sign or better + } // end if: white space, equals sign or better + else { + // There's a bad input character in the Base64 stream. + throw new IOException( String.format( + "Bad Base64 input character '%c' in array position %d", source[i], i ) ); + } // end else: + } // each input character + + byte[] out = new byte[ outBuffPosn ]; + System.arraycopy( outBuff, 0, out, 0, outBuffPosn ); + return out; + } // end decode + + + + + /** + * Decodes data from Base64 notation, automatically + * detecting gzip-compressed data and decompressing it. + * + * @param s the string to decode + * @return the decoded data + * @throws java.io.IOException If there is a problem + * @since 1.4 + */ + public static byte[] decode( String s ) throws IOException { + return decode( s, NO_OPTIONS ); + } + + + public static byte[] decodeWithoutPadding(String source) throws IOException { + int padding = source.length() % 4; + + if (padding == 1) source = source + "="; + else if (padding == 2) source = source + "=="; + else if (padding == 3) source = source + "="; + + return decode(source); + } + + + + /** + * Decodes data from Base64 notation, automatically + * detecting gzip-compressed data and decompressing it. + * + * @param s the string to decode + * @param options encode options such as URL_SAFE + * @return the decoded data + * @throws java.io.IOException if there is an error + * @throws NullPointerException if s is null + * @since 1.4 + */ + public static byte[] decode( String s, int options ) throws IOException { + + if( s == null ){ + throw new NullPointerException( "Input string was null." ); + } // end if + + byte[] bytes; + try { + bytes = s.getBytes( PREFERRED_ENCODING ); + } // end try + catch( java.io.UnsupportedEncodingException uee ) { + bytes = s.getBytes(); + } // end catch + // + + // Decode + bytes = decode( bytes, 0, bytes.length, options ); + + // Check to see if it's gzip-compressed + // GZIP Magic Two-Byte Number: 0x8b1f (35615) + boolean dontGunzip = (options & DONT_GUNZIP) != 0; + if( (bytes != null) && (bytes.length >= 4) && (!dontGunzip) ) { + + int head = ((int)bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00); + if( java.util.zip.GZIPInputStream.GZIP_MAGIC == head ) { + java.io.ByteArrayInputStream bais = null; + java.util.zip.GZIPInputStream gzis = null; + java.io.ByteArrayOutputStream baos = null; + byte[] buffer = new byte[2048]; + int length = 0; + + try { + baos = new java.io.ByteArrayOutputStream(); + bais = new java.io.ByteArrayInputStream( bytes ); + gzis = new java.util.zip.GZIPInputStream( bais ); + + while( ( length = gzis.read( buffer ) ) >= 0 ) { + baos.write(buffer,0,length); + } // end while: reading input + + // No error? Get new bytes. + bytes = baos.toByteArray(); + + } // end try + catch( IOException e ) { + // Just return originally-decoded bytes + } // end catch + finally { + try{ baos.close(); } catch( Exception e ){} + try{ gzis.close(); } catch( Exception e ){} + try{ bais.close(); } catch( Exception e ){} + } // end finally + + } // end if: gzipped + } // end if: bytes.length >= 2 + + return bytes; + } // end decode + + + + /** + * Attempts to decode Base64 data and deserialize a Java + * Object within. Returns null if there was an error. + * + * @param encodedObject The Base64 data to decode + * @return The decoded and deserialized object + * @throws NullPointerException if encodedObject is null + * @throws java.io.IOException if there is a general error + * @throws ClassNotFoundException if the decoded object is of a + * class that cannot be found by the JVM + * @since 1.5 + */ + public static Object decodeToObject( String encodedObject ) + throws IOException, ClassNotFoundException { + return decodeToObject(encodedObject,NO_OPTIONS,null); + } + + + /** + * Attempts to decode Base64 data and deserialize a Java + * Object within. Returns null if there was an error. + * If loader is not null, it will be the class loader + * used when deserializing. + * + * @param encodedObject The Base64 data to decode + * @param options Various parameters related to decoding + * @param loader Optional class loader to use in deserializing classes. + * @return The decoded and deserialized object + * @throws NullPointerException if encodedObject is null + * @throws java.io.IOException if there is a general error + * @throws ClassNotFoundException if the decoded object is of a + * class that cannot be found by the JVM + * @since 2.3.4 + */ + public static Object decodeToObject( + String encodedObject, int options, final ClassLoader loader ) + throws IOException, ClassNotFoundException { + + // Decode and gunzip if necessary + byte[] objBytes = decode( encodedObject, options ); + + java.io.ByteArrayInputStream bais = null; + java.io.ObjectInputStream ois = null; + Object obj = null; + + try { + bais = new java.io.ByteArrayInputStream( objBytes ); + + // If no custom class loader is provided, use Java's builtin OIS. + if( loader == null ){ + ois = new java.io.ObjectInputStream( bais ); + } // end if: no loader provided + + // Else make a customized object input stream that uses + // the provided class loader. + else { + ois = new java.io.ObjectInputStream(bais){ + @Override + public Class resolveClass(java.io.ObjectStreamClass streamClass) + throws IOException, ClassNotFoundException { + Class c = Class.forName(streamClass.getName(), false, loader); + if( c == null ){ + return super.resolveClass(streamClass); + } else { + return c; // Class loader knows of this class. + } // end else: not null + } // end resolveClass + }; // end ois + } // end else: no custom class loader + + obj = ois.readObject(); + } // end try + catch( IOException e ) { + throw e; // Catch and throw in order to execute finally{} + } // end catch + catch( ClassNotFoundException e ) { + throw e; // Catch and throw in order to execute finally{} + } // end catch + finally { + try{ bais.close(); } catch( Exception e ){} + try{ ois.close(); } catch( Exception e ){} + } // end finally + + return obj; + } // end decodeObject + + + + /** + * Convenience method for encoding data to a file. + * + *

As of v 2.3, if there is a error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned false, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param dataToEncode byte array of data to encode in base64 form + * @param filename Filename for saving encoded data + * @throws java.io.IOException if there is an error + * @throws NullPointerException if dataToEncode is null + * @since 2.1 + */ + public static void encodeToFile( byte[] dataToEncode, String filename ) + throws IOException { + + if( dataToEncode == null ){ + throw new NullPointerException( "Data to encode was null." ); + } // end iff + + Base64.OutputStream bos = null; + try { + bos = new Base64.OutputStream( + new java.io.FileOutputStream( filename ), Base64.ENCODE ); + bos.write( dataToEncode ); + } // end try + catch( IOException e ) { + throw e; // Catch and throw to execute finally{} block + } // end catch: java.io.IOException + finally { + try{ bos.close(); } catch( Exception e ){} + } // end finally + + } // end encodeToFile + + + /** + * Convenience method for decoding data to a file. + * + *

As of v 2.3, if there is a error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned false, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param dataToDecode Base64-encoded data as a string + * @param filename Filename for saving decoded data + * @throws java.io.IOException if there is an error + * @since 2.1 + */ + public static void decodeToFile( String dataToDecode, String filename ) + throws IOException { + + Base64.OutputStream bos = null; + try{ + bos = new Base64.OutputStream( + new java.io.FileOutputStream( filename ), Base64.DECODE ); + bos.write( dataToDecode.getBytes( PREFERRED_ENCODING ) ); + } // end try + catch( IOException e ) { + throw e; // Catch and throw to execute finally{} block + } // end catch: java.io.IOException + finally { + try{ bos.close(); } catch( Exception e ){} + } // end finally + + } // end decodeToFile + + + + + /** + * Convenience method for reading a base64-encoded + * file and decoding it. + * + *

As of v 2.3, if there is a error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned false, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param filename Filename for reading encoded data + * @return decoded byte array + * @throws java.io.IOException if there is an error + * @since 2.1 + */ + public static byte[] decodeFromFile( String filename ) + throws IOException { + + byte[] decodedData = null; + Base64.InputStream bis = null; + try + { + // Set up some useful variables + java.io.File file = new java.io.File( filename ); + byte[] buffer = null; + int length = 0; + int numBytes = 0; + + // Check for size of file + if( file.length() > Integer.MAX_VALUE ) + { + throw new IOException( "File is too big for this convenience method (" + file.length() + " bytes)." ); + } // end if: file too big for int index + buffer = new byte[ (int)file.length() ]; + + // Open a stream + bis = new Base64.InputStream( + new java.io.BufferedInputStream( + new java.io.FileInputStream( file ) ), Base64.DECODE ); + + // Read until done + while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) { + length += numBytes; + } // end while + + // Save in a variable to return + decodedData = new byte[ length ]; + System.arraycopy( buffer, 0, decodedData, 0, length ); + + } // end try + catch( IOException e ) { + throw e; // Catch and release to execute finally{} + } // end catch: java.io.IOException + finally { + try{ bis.close(); } catch( Exception e) {} + } // end finally + + return decodedData; + } // end decodeFromFile + + + + /** + * Convenience method for reading a binary file + * and base64-encoding it. + * + *

As of v 2.3, if there is a error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned false, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param filename Filename for reading binary data + * @return base64-encoded string + * @throws java.io.IOException if there is an error + * @since 2.1 + */ + public static String encodeFromFile( String filename ) + throws IOException { + + String encodedData = null; + Base64.InputStream bis = null; + try + { + // Set up some useful variables + java.io.File file = new java.io.File( filename ); + byte[] buffer = new byte[ Math.max((int)(file.length() * 1.4),40) ]; // Need max() for math on small files (v2.2.1) + int length = 0; + int numBytes = 0; + + // Open a stream + bis = new Base64.InputStream( + new java.io.BufferedInputStream( + new java.io.FileInputStream( file ) ), Base64.ENCODE ); + + // Read until done + while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) { + length += numBytes; + } // end while + + // Save in a variable to return + encodedData = new String( buffer, 0, length, Base64.PREFERRED_ENCODING ); + + } // end try + catch( IOException e ) { + throw e; // Catch and release to execute finally{} + } // end catch: java.io.IOException + finally { + try{ bis.close(); } catch( Exception e) {} + } // end finally + + return encodedData; + } // end encodeFromFile + + /** + * Reads infile and encodes it to outfile. + * + * @param infile Input file + * @param outfile Output file + * @throws java.io.IOException if there is an error + * @since 2.2 + */ + public static void encodeFileToFile( String infile, String outfile ) + throws IOException { + + String encoded = Base64.encodeFromFile( infile ); + java.io.OutputStream out = null; + try{ + out = new java.io.BufferedOutputStream( + new java.io.FileOutputStream( outfile ) ); + out.write( encoded.getBytes("US-ASCII") ); // Strict, 7-bit output. + } // end try + catch( IOException e ) { + throw e; // Catch and release to execute finally{} + } // end catch + finally { + try { out.close(); } + catch( Exception ex ){} + } // end finally + } // end encodeFileToFile + + + /** + * Reads infile and decodes it to outfile. + * + * @param infile Input file + * @param outfile Output file + * @throws java.io.IOException if there is an error + * @since 2.2 + */ + public static void decodeFileToFile( String infile, String outfile ) + throws IOException { + + byte[] decoded = Base64.decodeFromFile( infile ); + java.io.OutputStream out = null; + try{ + out = new java.io.BufferedOutputStream( + new java.io.FileOutputStream( outfile ) ); + out.write( decoded ); + } // end try + catch( IOException e ) { + throw e; // Catch and release to execute finally{} + } // end catch + finally { + try { out.close(); } + catch( Exception ex ){} + } // end finally + } // end decodeFileToFile + + + /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */ + + + + /** + * A {@link Base64.InputStream} will read data from another + * java.io.InputStream, given in the constructor, + * and encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class InputStream extends java.io.FilterInputStream { + + private boolean encode; // Encoding or decoding + private int position; // Current position in the buffer + private byte[] buffer; // Small buffer holding converted data + private int bufferLength; // Length of buffer (3 or 4) + private int numSigBytes; // Number of meaningful bytes in the buffer + private int lineLength; + private boolean breakLines; // Break lines at less than 80 characters + private int options; // Record options used to create the stream. + private byte[] decodabet; // Local copies to avoid extra method calls + + + /** + * Constructs a {@link Base64.InputStream} in DECODE mode. + * + * @param in the java.io.InputStream from which to read data. + * @since 1.3 + */ + public InputStream( java.io.InputStream in ) { + this( in, DECODE ); + } // end constructor + + + /** + * Constructs a {@link Base64.InputStream} in + * either ENCODE or DECODE mode. + *

+ * Valid options:

+         *   ENCODE or DECODE: Encode or Decode as data is read.
+         *   DO_BREAK_LINES: break lines at 76 characters
+         *     (only meaningful when encoding)
+         * 
+ *

+ * Example: new Base64.InputStream( in, Base64.DECODE ) + * + * + * @param in the java.io.InputStream from which to read data. + * @param options Specified options + * @see Base64#ENCODE + * @see Base64#DECODE + * @see Base64#DO_BREAK_LINES + * @since 2.0 + */ + public InputStream( java.io.InputStream in, int options ) { + + super( in ); + this.options = options; // Record for later + this.breakLines = (options & DO_BREAK_LINES) > 0; + this.encode = (options & ENCODE) > 0; + this.bufferLength = encode ? 4 : 3; + this.buffer = new byte[ bufferLength ]; + this.position = -1; + this.lineLength = 0; + this.decodabet = getDecodabet(options); + } // end constructor + + /** + * Reads enough of the input stream to convert + * to/from Base64 and returns the next byte. + * + * @return next byte + * @since 1.3 + */ + @Override + public int read() throws IOException { + + // Do we need to get data? + if( position < 0 ) { + if( encode ) { + byte[] b3 = new byte[3]; + int numBinaryBytes = 0; + for( int i = 0; i < 3; i++ ) { + int b = in.read(); + + // If end of stream, b is -1. + if( b >= 0 ) { + b3[i] = (byte)b; + numBinaryBytes++; + } else { + break; // out of for loop + } // end else: end of stream + + } // end for: each needed input byte + + if( numBinaryBytes > 0 ) { + encode3to4( b3, 0, numBinaryBytes, buffer, 0, options ); + position = 0; + numSigBytes = 4; + } // end if: got data + else { + return -1; // Must be end of stream + } // end else + } // end if: encoding + + // Else decoding + else { + byte[] b4 = new byte[4]; + int i = 0; + for( i = 0; i < 4; i++ ) { + // Read four "meaningful" bytes: + int b = 0; + do{ b = in.read(); } + while( b >= 0 && decodabet[ b & 0x7f ] <= WHITE_SPACE_ENC ); + + if( b < 0 ) { + break; // Reads a -1 if end of stream + } // end if: end of stream + + b4[i] = (byte)b; + } // end for: each needed input byte + + if( i == 4 ) { + numSigBytes = decode4to3( b4, 0, buffer, 0, options ); + position = 0; + } // end if: got four characters + else if( i == 0 ){ + return -1; + } // end else if: also padded correctly + else { + // Must have broken out from above. + throw new IOException( "Improperly padded Base64 input." ); + } // end + + } // end else: decode + } // end else: get data + + // Got data? + if( position >= 0 ) { + // End of relevant data? + if( /*!encode &&*/ position >= numSigBytes ){ + return -1; + } // end if: got data + + if( encode && breakLines && lineLength >= MAX_LINE_LENGTH ) { + lineLength = 0; + return '\n'; + } // end if + else { + lineLength++; // This isn't important when decoding + // but throwing an extra "if" seems + // just as wasteful. + + int b = buffer[ position++ ]; + + if( position >= bufferLength ) { + position = -1; + } // end if: end + + return b & 0xFF; // This is how you "cast" a byte that's + // intended to be unsigned. + } // end else + } // end if: position >= 0 + + // Else error + else { + throw new IOException( "Error in Base64 code reading stream." ); + } // end else + } // end read + + + /** + * Calls {@link #read()} repeatedly until the end of stream + * is reached or len bytes are read. + * Returns number of bytes read into array or -1 if + * end of stream is encountered. + * + * @param dest array to hold values + * @param off offset for array + * @param len max number of bytes to read into array + * @return bytes read into array or -1 if end of stream is encountered. + * @since 1.3 + */ + @Override + public int read( byte[] dest, int off, int len ) + throws IOException { + int i; + int b; + for( i = 0; i < len; i++ ) { + b = read(); + + if( b >= 0 ) { + dest[off + i] = (byte) b; + } + else if( i == 0 ) { + return -1; + } + else { + break; // Out of 'for' loop + } // Out of 'for' loop + } // end for: each byte read + return i; + } // end read + + } // end inner class InputStream + + + + + + + /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */ + + + + /** + * A {@link Base64.OutputStream} will write data to another + * java.io.OutputStream, given in the constructor, + * and encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class OutputStream extends java.io.FilterOutputStream { + + private boolean encode; + private int position; + private byte[] buffer; + private int bufferLength; + private int lineLength; + private boolean breakLines; + private byte[] b4; // Scratch used in a few places + private boolean suspendEncoding; + private int options; // Record for later + private byte[] decodabet; // Local copies to avoid extra method calls + + /** + * Constructs a {@link Base64.OutputStream} in ENCODE mode. + * + * @param out the java.io.OutputStream to which data will be written. + * @since 1.3 + */ + public OutputStream( java.io.OutputStream out ) { + this( out, ENCODE ); + } // end constructor + + + /** + * Constructs a {@link Base64.OutputStream} in + * either ENCODE or DECODE mode. + *

+ * Valid options:

+         *   ENCODE or DECODE: Encode or Decode as data is read.
+         *   DO_BREAK_LINES: don't break lines at 76 characters
+         *     (only meaningful when encoding)
+         * 
+ *

+ * Example: new Base64.OutputStream( out, Base64.ENCODE ) + * + * @param out the java.io.OutputStream to which data will be written. + * @param options Specified options. + * @see Base64#ENCODE + * @see Base64#DECODE + * @see Base64#DO_BREAK_LINES + * @since 1.3 + */ + public OutputStream( java.io.OutputStream out, int options ) { + super( out ); + this.breakLines = (options & DO_BREAK_LINES) != 0; + this.encode = (options & ENCODE) != 0; + this.bufferLength = encode ? 3 : 4; + this.buffer = new byte[ bufferLength ]; + this.position = 0; + this.lineLength = 0; + this.suspendEncoding = false; + this.b4 = new byte[4]; + this.options = options; + this.decodabet = getDecodabet(options); + } // end constructor + + + /** + * Writes the byte to the output stream after + * converting to/from Base64 notation. + * When encoding, bytes are buffered three + * at a time before the output stream actually + * gets a write() call. + * When decoding, bytes are buffered four + * at a time. + * + * @param theByte the byte to write + * @since 1.3 + */ + @Override + public void write(int theByte) + throws IOException { + // Encoding suspended? + if( suspendEncoding ) { + this.out.write( theByte ); + return; + } // end if: supsended + + // Encode? + if( encode ) { + buffer[ position++ ] = (byte)theByte; + if( position >= bufferLength ) { // Enough to encode. + + this.out.write( encode3to4( b4, buffer, bufferLength, options ) ); + + lineLength += 4; + if( breakLines && lineLength >= MAX_LINE_LENGTH ) { + this.out.write( NEW_LINE ); + lineLength = 0; + } // end if: end of line + + position = 0; + } // end if: enough to output + } // end if: encoding + + // Else, Decoding + else { + // Meaningful Base64 character? + if( decodabet[ theByte & 0x7f ] > WHITE_SPACE_ENC ) { + buffer[ position++ ] = (byte)theByte; + if( position >= bufferLength ) { // Enough to output. + + int len = Base64.decode4to3( buffer, 0, b4, 0, options ); + out.write( b4, 0, len ); + position = 0; + } // end if: enough to output + } // end if: meaningful base64 character + else if( decodabet[ theByte & 0x7f ] != WHITE_SPACE_ENC ) { + throw new IOException( "Invalid character in Base64 data." ); + } // end else: not white space either + } // end else: decoding + } // end write + + + + /** + * Calls {@link #write(int)} repeatedly until len + * bytes are written. + * + * @param theBytes array from which to read bytes + * @param off offset for array + * @param len max number of bytes to read into array + * @since 1.3 + */ + @Override + public void write( byte[] theBytes, int off, int len ) + throws IOException { + // Encoding suspended? + if( suspendEncoding ) { + this.out.write( theBytes, off, len ); + return; + } // end if: supsended + + for( int i = 0; i < len; i++ ) { + write( theBytes[ off + i ] ); + } // end for: each byte written + + } // end write + + + + /** + * Method added by PHIL. [Thanks, PHIL. -Rob] + * This pads the buffer without closing the stream. + * @throws java.io.IOException if there's an error. + */ + public void flushBase64() throws IOException { + if( position > 0 ) { + if( encode ) { + out.write( encode3to4( b4, buffer, position, options ) ); + position = 0; + } // end if: encoding + else { + throw new IOException( "Base64 input not properly padded." ); + } // end else: decoding + } // end if: buffer partially full + + } // end flush + + + /** + * Flushes and closes (I think, in the superclass) the stream. + * + * @since 1.3 + */ + @Override + public void close() throws IOException { + // 1. Ensure that pending characters are written + flushBase64(); + + // 2. Actually close the stream + // Base class both flushes and closes. + super.close(); + + buffer = null; + out = null; + } // end close + + + + /** + * Suspends encoding of the stream. + * May be helpful if you need to embed a piece of + * base64-encoded data in a stream. + * + * @throws java.io.IOException if there's an error flushing + * @since 1.5.1 + */ + public void suspendEncoding() throws IOException { + flushBase64(); + this.suspendEncoding = true; + } // end suspendEncoding + + + /** + * Resumes encoding of the stream. + * May be helpful if you need to embed a piece of + * base64-encoded data in a stream. + * + * @since 1.5.1 + */ + public void resumeEncoding() { + this.suspendEncoding = false; + } // end resumeEncoding + + + + } // end inner class OutputStream + + +} // end class Base64 diff --git a/flock/src/main/java/org/anhonesteffort/flock/util/ColorUtils.java b/flock/src/main/java/org/anhonesteffort/flock/util/ColorUtils.java new file mode 100644 index 0000000..05366bb --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/util/ColorUtils.java @@ -0,0 +1,243 @@ +package org.anhonesteffort.flock.util; + +import java.util.ArrayList; + +/** + * Java Code to get a color name from rgb/hex value/awt color + * + * The part of looking up a color name from the rgb values is edited from + * https://gist.github.com/nightlark/6482130#file-gistfile1-java (that has some errors) by Ryan Mast (nightlark) + * + * @author Xiaoxiao Li + * + */ +public class ColorUtils { + + /** + * Initialize the color list that we have. + */ + private ArrayList initColorList() { + ArrayList colorList = new ArrayList(); + colorList.add(new ColorName("alice blue", 0xF0, 0xF8, 0xFF)); + colorList.add(new ColorName("antique white", 0xFA, 0xEB, 0xD7)); + colorList.add(new ColorName("aqua", 0x00, 0xFF, 0xFF)); + colorList.add(new ColorName("aquamarine", 0x7F, 0xFF, 0xD4)); + colorList.add(new ColorName("azure", 0xF0, 0xFF, 0xFF)); + colorList.add(new ColorName("beige", 0xF5, 0xF5, 0xDC)); + colorList.add(new ColorName("bisque", 0xFF, 0xE4, 0xC4)); + colorList.add(new ColorName("black", 0x00, 0x00, 0x00)); + colorList.add(new ColorName("blanched almond", 0xFF, 0xEB, 0xCD)); + colorList.add(new ColorName("blue", 0x00, 0x00, 0xFF)); + colorList.add(new ColorName("blue violet", 0x8A, 0x2B, 0xE2)); + colorList.add(new ColorName("brown", 0xA5, 0x2A, 0x2A)); + colorList.add(new ColorName("burly wood", 0xDE, 0xB8, 0x87)); + colorList.add(new ColorName("cadet blue", 0x5F, 0x9E, 0xA0)); + colorList.add(new ColorName("chartreuse", 0x7F, 0xFF, 0x00)); + colorList.add(new ColorName("chocolate", 0xD2, 0x69, 0x1E)); + colorList.add(new ColorName("coral", 0xFF, 0x7F, 0x50)); + colorList.add(new ColorName("cornflower blue", 0x64, 0x95, 0xED)); + colorList.add(new ColorName("cornsilk", 0xFF, 0xF8, 0xDC)); + colorList.add(new ColorName("crimson", 0xDC, 0x14, 0x3C)); + colorList.add(new ColorName("cyan", 0x00, 0xFF, 0xFF)); + colorList.add(new ColorName("dark blue", 0x00, 0x00, 0x8B)); + colorList.add(new ColorName("dark cyan", 0x00, 0x8B, 0x8B)); + colorList.add(new ColorName("dark golden rod", 0xB8, 0x86, 0x0B)); + colorList.add(new ColorName("dark gray", 0xA9, 0xA9, 0xA9)); + colorList.add(new ColorName("dark green", 0x00, 0x64, 0x00)); + colorList.add(new ColorName("dark khaki", 0xBD, 0xB7, 0x6B)); + colorList.add(new ColorName("dark magenta", 0x8B, 0x00, 0x8B)); + colorList.add(new ColorName("dark olive green", 0x55, 0x6B, 0x2F)); + colorList.add(new ColorName("dark orange", 0xFF, 0x8C, 0x00)); + colorList.add(new ColorName("dark orchid", 0x99, 0x32, 0xCC)); + colorList.add(new ColorName("dark red", 0x8B, 0x00, 0x00)); + colorList.add(new ColorName("dark salmon", 0xE9, 0x96, 0x7A)); + colorList.add(new ColorName("dark sea green", 0x8F, 0xBC, 0x8F)); + colorList.add(new ColorName("dark slate blue", 0x48, 0x3D, 0x8B)); + colorList.add(new ColorName("dark slate gray", 0x2F, 0x4F, 0x4F)); + colorList.add(new ColorName("dark turquoise", 0x00, 0xCE, 0xD1)); + colorList.add(new ColorName("dark violet", 0x94, 0x00, 0xD3)); + colorList.add(new ColorName("deep pink", 0xFF, 0x14, 0x93)); + colorList.add(new ColorName("deep sky blue", 0x00, 0xBF, 0xFF)); + colorList.add(new ColorName("dim gray", 0x69, 0x69, 0x69)); + colorList.add(new ColorName("dodger blue", 0x1E, 0x90, 0xFF)); + colorList.add(new ColorName("fire brick", 0xB2, 0x22, 0x22)); + colorList.add(new ColorName("floral white", 0xFF, 0xFA, 0xF0)); + colorList.add(new ColorName("forest green", 0x22, 0x8B, 0x22)); + colorList.add(new ColorName("fuchsia", 0xFF, 0x00, 0xFF)); + colorList.add(new ColorName("gainsboro", 0xDC, 0xDC, 0xDC)); + colorList.add(new ColorName("ghost white", 0xF8, 0xF8, 0xFF)); + colorList.add(new ColorName("gold", 0xFF, 0xD7, 0x00)); + colorList.add(new ColorName("golden rod", 0xDA, 0xA5, 0x20)); + colorList.add(new ColorName("gray", 0x80, 0x80, 0x80)); + colorList.add(new ColorName("green", 0x00, 0x80, 0x00)); + colorList.add(new ColorName("green yellow", 0xAD, 0xFF, 0x2F)); + colorList.add(new ColorName("honey dew", 0xF0, 0xFF, 0xF0)); + colorList.add(new ColorName("hot pink", 0xFF, 0x69, 0xB4)); + colorList.add(new ColorName("indian red", 0xCD, 0x5C, 0x5C)); + colorList.add(new ColorName("indigo", 0x4B, 0x00, 0x82)); + colorList.add(new ColorName("ivory", 0xFF, 0xFF, 0xF0)); + colorList.add(new ColorName("khaki", 0xF0, 0xE6, 0x8C)); + colorList.add(new ColorName("lavender", 0xE6, 0xE6, 0xFA)); + colorList.add(new ColorName("lavender blush", 0xFF, 0xF0, 0xF5)); + colorList.add(new ColorName("lawn green", 0x7C, 0xFC, 0x00)); + colorList.add(new ColorName("lemon chiffon", 0xFF, 0xFA, 0xCD)); + colorList.add(new ColorName("light blue", 0xAD, 0xD8, 0xE6)); + colorList.add(new ColorName("light coral", 0xF0, 0x80, 0x80)); + colorList.add(new ColorName("light cyan", 0xE0, 0xFF, 0xFF)); + colorList.add(new ColorName("light golden rod yellow", 0xFA, 0xFA, 0xD2)); + colorList.add(new ColorName("light gray", 0xD3, 0xD3, 0xD3)); + colorList.add(new ColorName("light green", 0x90, 0xEE, 0x90)); + colorList.add(new ColorName("light pink", 0xFF, 0xB6, 0xC1)); + colorList.add(new ColorName("light salmon", 0xFF, 0xA0, 0x7A)); + colorList.add(new ColorName("light sea green", 0x20, 0xB2, 0xAA)); + colorList.add(new ColorName("light sky blue", 0x87, 0xCE, 0xFA)); + colorList.add(new ColorName("light slate gray", 0x77, 0x88, 0x99)); + colorList.add(new ColorName("light steel blue", 0xB0, 0xC4, 0xDE)); + colorList.add(new ColorName("light yellow", 0xFF, 0xFF, 0xE0)); + colorList.add(new ColorName("lime", 0x00, 0xFF, 0x00)); + colorList.add(new ColorName("lime green", 0x32, 0xCD, 0x32)); + colorList.add(new ColorName("linen", 0xFA, 0xF0, 0xE6)); + colorList.add(new ColorName("magenta", 0xFF, 0x00, 0xFF)); + colorList.add(new ColorName("maroon", 0x80, 0x00, 0x00)); + colorList.add(new ColorName("medium aqua marine", 0x66, 0xCD, 0xAA)); + colorList.add(new ColorName("medium blue", 0x00, 0x00, 0xCD)); + colorList.add(new ColorName("medium orchid", 0xBA, 0x55, 0xD3)); + colorList.add(new ColorName("medium purple", 0x93, 0x70, 0xDB)); + colorList.add(new ColorName("medium sea green", 0x3C, 0xB3, 0x71)); + colorList.add(new ColorName("medium slate blue", 0x7B, 0x68, 0xEE)); + colorList.add(new ColorName("medium spring green", 0x00, 0xFA, 0x9A)); + colorList.add(new ColorName("medium turquoise", 0x48, 0xD1, 0xCC)); + colorList.add(new ColorName("medium violet red", 0xC7, 0x15, 0x85)); + colorList.add(new ColorName("midnight blue", 0x19, 0x19, 0x70)); + colorList.add(new ColorName("mint cream", 0xF5, 0xFF, 0xFA)); + colorList.add(new ColorName("misty rose", 0xFF, 0xE4, 0xE1)); + colorList.add(new ColorName("moccasin", 0xFF, 0xE4, 0xB5)); + colorList.add(new ColorName("navajo white", 0xFF, 0xDE, 0xAD)); + colorList.add(new ColorName("navy", 0x00, 0x00, 0x80)); + colorList.add(new ColorName("old lace", 0xFD, 0xF5, 0xE6)); + colorList.add(new ColorName("olive", 0x80, 0x80, 0x00)); + colorList.add(new ColorName("olive drab", 0x6B, 0x8E, 0x23)); + colorList.add(new ColorName("orange", 0xFF, 0xA5, 0x00)); + colorList.add(new ColorName("orange red", 0xFF, 0x45, 0x00)); + colorList.add(new ColorName("orchid", 0xDA, 0x70, 0xD6)); + colorList.add(new ColorName("pale golden rod", 0xEE, 0xE8, 0xAA)); + colorList.add(new ColorName("pale green", 0x98, 0xFB, 0x98)); + colorList.add(new ColorName("pale turquoise", 0xAF, 0xEE, 0xEE)); + colorList.add(new ColorName("pale violet red", 0xDB, 0x70, 0x93)); + colorList.add(new ColorName("papaya whip", 0xFF, 0xEF, 0xD5)); + colorList.add(new ColorName("peach puff", 0xFF, 0xDA, 0xB9)); + colorList.add(new ColorName("peru", 0xCD, 0x85, 0x3F)); + colorList.add(new ColorName("pink", 0xFF, 0xC0, 0xCB)); + colorList.add(new ColorName("plum", 0xDD, 0xA0, 0xDD)); + colorList.add(new ColorName("powder blue", 0xB0, 0xE0, 0xE6)); + colorList.add(new ColorName("purple", 0x80, 0x00, 0x80)); + colorList.add(new ColorName("red", 0xFF, 0x00, 0x00)); + colorList.add(new ColorName("rosy brown", 0xBC, 0x8F, 0x8F)); + colorList.add(new ColorName("royal blue", 0x41, 0x69, 0xE1)); + colorList.add(new ColorName("saddle brown", 0x8B, 0x45, 0x13)); + colorList.add(new ColorName("salmon", 0xFA, 0x80, 0x72)); + colorList.add(new ColorName("sandy brown", 0xF4, 0xA4, 0x60)); + colorList.add(new ColorName("sea green", 0x2E, 0x8B, 0x57)); + colorList.add(new ColorName("sea shell", 0xFF, 0xF5, 0xEE)); + colorList.add(new ColorName("sienna", 0xA0, 0x52, 0x2D)); + colorList.add(new ColorName("silver", 0xC0, 0xC0, 0xC0)); + colorList.add(new ColorName("sky blue", 0x87, 0xCE, 0xEB)); + colorList.add(new ColorName("slate blue", 0x6A, 0x5A, 0xCD)); + colorList.add(new ColorName("slate gray", 0x70, 0x80, 0x90)); + colorList.add(new ColorName("snow", 0xFF, 0xFA, 0xFA)); + colorList.add(new ColorName("spring green", 0x00, 0xFF, 0x7F)); + colorList.add(new ColorName("steel blue", 0x46, 0x82, 0xB4)); + colorList.add(new ColorName("tan", 0xD2, 0xB4, 0x8C)); + colorList.add(new ColorName("teal", 0x00, 0x80, 0x80)); + colorList.add(new ColorName("thistle", 0xD8, 0xBF, 0xD8)); + colorList.add(new ColorName("tomato", 0xFF, 0x63, 0x47)); + colorList.add(new ColorName("turquoise", 0x40, 0xE0, 0xD0)); + colorList.add(new ColorName("violet", 0xEE, 0x82, 0xEE)); + colorList.add(new ColorName("wheat", 0xF5, 0xDE, 0xB3)); + colorList.add(new ColorName("white", 0xFF, 0xFF, 0xFF)); + colorList.add(new ColorName("white smoke", 0xF5, 0xF5, 0xF5)); + colorList.add(new ColorName("yellow", 0xFF, 0xFF, 0x00)); + colorList.add(new ColorName("yellow green", 0x9A, 0xCD, 0x32)); + return colorList; + } + + /** + * Get the closest color name from our list + * + * @param r + * @param g + * @param b + * @return + */ + public String getColorNameFromRgb(int r, int g, int b) { + ArrayList colorList = initColorList(); + ColorName closestMatch = null; + int minMSE = Integer.MAX_VALUE; + int mse; + for (ColorName c : colorList) { + mse = c.computeMSE(r, g, b); + if (mse < minMSE) { + minMSE = mse; + closestMatch = c; + } + } + + if (closestMatch != null) { + return closestMatch.getName(); + } else { + return "no matched color name."; + } + } + + /** + * Convert hexColor to rgb, then call getColorNameFromRgb(r, g, b) + * + * @param hexColor + * @return + */ + public String getColorNameFromHex(int hexColor) { + int r = (hexColor & 0xFF0000) >> 16; + int g = (hexColor & 0xFF00) >> 8; + int b = (hexColor & 0xFF); + return getColorNameFromRgb(r, g, b); + } + + /** + * SubClass of ColorUtils. In order to lookup color name + * + * @author Xiaoxiao Li + * + */ + public class ColorName { + public int r, g, b; + public String name; + + public ColorName(String name, int r, int g, int b) { + this.r = r; + this.g = g; + this.b = b; + this.name = name; + } + + public int computeMSE(int pixR, int pixG, int pixB) { + return (int) (((pixR - r) * (pixR - r) + (pixG - g) * (pixG - g) + (pixB - b) + * (pixB - b)) / 3); + } + + public int getR() { + return r; + } + + public int getG() { + return g; + } + + public int getB() { + return b; + } + + public String getName() { + return name; + } + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/util/PasswordUtil.java b/flock/src/main/java/org/anhonesteffort/flock/util/PasswordUtil.java new file mode 100644 index 0000000..f32be51 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/util/PasswordUtil.java @@ -0,0 +1,78 @@ +package org.anhonesteffort.flock.util; + +import android.content.Context; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.widget.ProgressBar; + +import org.anhonesteffort.flock.R; + +/** + * Created by rhodey. + */ +public class PasswordUtil { + + // it works, ok? + public static int getPasswordStrength(String password) { + int passwordStrength = 1; + + passwordStrength += (password.length() / 3); + + if (!password.toLowerCase().equals(password)) + passwordStrength += 2; + + int integerCount = 0; + for (int i = 0; i < password.length(); i++) { + if (Character.isDigit(password.charAt(i))) + integerCount++; + } + + if (integerCount > 0 && integerCount != password.length()) + passwordStrength += (integerCount / 2); + + return passwordStrength; + } + + public static void handleUpdateProgressWithPasswordStrength(Context context, + String password, + ProgressBar progressBar) + { + progressBar.setMax(10); + + if (password.length() == 0) + progressBar.setVisibility(View.INVISIBLE); + else + progressBar.setVisibility(View.VISIBLE); + + int passwordStrength = PasswordUtil.getPasswordStrength(password); + progressBar.setProgress(passwordStrength); + + if (passwordStrength > 6) + progressBar.setProgressDrawable(context.getResources().getDrawable(R.drawable.flocktheme_progress_horizontal_holo_light_green)); + else if (passwordStrength > 3) + progressBar.setProgressDrawable(context.getResources().getDrawable(R.drawable.flocktheme_progress_horizontal_holo_light_yellow)); + else + progressBar.setProgressDrawable(context.getResources().getDrawable(R.drawable.flocktheme_progress_horizontal_holo_light_red)); + } + + public static TextWatcher getPasswordStrengthTextWatcher(final Context context, + final ProgressBar progressBar) + { + return new TextWatcher() { + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { } + + @Override + public void afterTextChanged(Editable s) { + PasswordUtil.handleUpdateProgressWithPasswordStrength(context, s.toString(), progressBar); + } + + }; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/util/Util.java b/flock/src/main/java/org/anhonesteffort/flock/util/Util.java new file mode 100644 index 0000000..9c1fa48 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/util/Util.java @@ -0,0 +1,51 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ +package org.anhonesteffort.flock.util; + +public class Util { + + public static byte[] combine(byte[] one, byte[] two) { + byte[] combined = new byte[one.length + two.length]; + System.arraycopy(one, 0, combined, 0, one.length); + System.arraycopy(two, 0, combined, one.length, two.length); + + return combined; + } + + public static byte[] combine(byte[] one, byte[] two, byte[] three) { + byte[] combined = new byte[one.length + two.length + three.length]; + System.arraycopy(one, 0, combined, 0, one.length); + System.arraycopy(two, 0, combined, one.length, two.length); + System.arraycopy(three, 0, combined, one.length + two.length, three.length); + + return combined; + } + + public static byte[] combine(byte[] one, byte[] two, byte[] three, byte[] four) { + byte[] combined = new byte[one.length + two.length + three.length + four.length]; + System.arraycopy(one, 0, combined, 0, one.length); + System.arraycopy(two, 0, combined, one.length, two.length); + System.arraycopy(three, 0, combined, one.length + two.length, three.length); + System.arraycopy(four, 0, combined, one.length + two.length + three.length, four.length); + + return combined; + + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/webdav/AbstractDavComponentCollection.java b/flock/src/main/java/org/anhonesteffort/flock/webdav/AbstractDavComponentCollection.java new file mode 100644 index 0000000..144a29b --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/webdav/AbstractDavComponentCollection.java @@ -0,0 +1,480 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.webdav; + +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.webdav.caldav.CalDavConstants; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.client.methods.DeleteMethod; +import org.apache.jackrabbit.webdav.client.methods.PropFindMethod; +import org.apache.jackrabbit.webdav.client.methods.PropPatchMethod; +import org.apache.jackrabbit.webdav.client.methods.ReportMethod; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.security.SecurityConstants; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; +import org.apache.jackrabbit.webdav.version.report.ReportType; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +/** + * Programmer: rhodey + */ +public abstract class AbstractDavComponentCollection implements DavComponentCollection { + + private final AbstractDavComponentStore store; + private String path; + protected DavPropertySet properties; + + protected AbstractDavComponentCollection(AbstractDavComponentStore store, String path) { + this.store = store; + this.path = path; + properties = new DavPropertySet(); + } + + protected AbstractDavComponentCollection(AbstractDavComponentStore store, + String path, + DavPropertySet properties) + { + this.store = store; + this.path = path; + this.properties = properties; + } + + protected AbstractDavComponentCollection(AbstractDavComponentStore store, + String path, + String displayName) + { + this.store = store; + this.path = path; + + properties = new DavPropertySet(); + properties.add(new DefaultDavProperty(DavPropertyName.DISPLAYNAME, displayName)); + } + + public AbstractDavComponentStore getStore() { + return store; + } + + @Override + public String getPath() { + if (!path.endsWith("/")) + path = path.concat("/"); + + return path; + } + + protected abstract String getComponentPathFromUid(String uid); + + protected Optional getUidFromComponentPath(String path) { + int extension_index = path.lastIndexOf("."); + int file_name_index = path.lastIndexOf("/"); + + if (file_name_index == -1 || extension_index == -1 || + file_name_index == (path.length() - 1)) + return Optional.absent(); + + return Optional.of(path.substring(file_name_index + 1, extension_index)); + } + + protected DavPropertyNameSet getPropertyNamesForFetch() { + DavPropertyNameSet baseProperties = new DavPropertyNameSet(); + baseProperties.add(DavPropertyName.RESOURCETYPE); + baseProperties.add(DavPropertyName.DISPLAYNAME); + baseProperties.add(SecurityConstants.OWNER); + + baseProperties.add(WebDavConstants.PROPERTY_NAME_PROP); + baseProperties.add(SecurityConstants.CURRENT_USER_PRIVILEGE_SET); + baseProperties.add(WebDavConstants.PROPERTY_NAME_QUOTA_AVAILABLE_BYTES); + baseProperties.add(WebDavConstants.PROPERTY_NAME_QUOTA_USED_BYTES); + baseProperties.add(WebDavConstants.PROPERTY_NAME_RESOURCE_ID); + baseProperties.add(WebDavConstants.PROPERTY_NAME_SUPPORTED_REPORT_SET); // TODO getter method for this. + baseProperties.add(WebDavConstants.PROPERTY_NAME_SYNC_TOKEN); + + baseProperties.add(CalDavConstants.PROPERTY_NAME_CTAG); + + return baseProperties; + } + + protected abstract ReportType getMultiGetReportType(); + + protected abstract ReportType getQueryReportType(); + + protected abstract DavPropertyNameSet getPropertyNamesForReports(); + + public DavPropertySet getProperties() { + return properties; + } + + // TODO: make this cleaner? + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public

Optional

getProperty(DavPropertyName propertyName, Class

type) + throws PropertyParseException + { + if (properties.get(propertyName) != null) { + try { + + Object value = properties.get(propertyName).getValue(); + + if (Collection.class.isAssignableFrom(type)) { + P result = type.newInstance(); + if (value instanceof Collection) + ((Collection) result).addAll((Collection) value); + return Optional.of(result); + } + else { + Constructor

constructor = type.getConstructor(value.getClass()); + return Optional.of(constructor.newInstance(value)); + } + + } catch (Exception e) { + throw new PropertyParseException("caught exception while getting property " + + propertyName.getName(), path, propertyName, e); + } + } + + return Optional.absent(); + } + + @Override + @SuppressWarnings("unchecked") + public List getResourceTypes() throws PropertyParseException { + List resourceTypes = new LinkedList(); + Optional resourceTypeProp = getProperty(DavPropertyName.RESOURCETYPE, ArrayList.class); + + if (!resourceTypeProp.isPresent()) + return resourceTypes; + + for (Node child : (ArrayList) resourceTypeProp.get()) { + if (child instanceof Element) { + String nameNode = child.getNodeName(); + if (nameNode != null) + resourceTypes.add(nameNode); + } + } + + return resourceTypes; + } + + @Override + public Optional getDisplayName() throws PropertyParseException { + return getProperty(DavPropertyName.DISPLAYNAME, String.class); + } + + @Override + public Optional getCTag() throws PropertyParseException { + return getProperty(CalDavConstants.PROPERTY_NAME_CTAG, String.class); + } + + @Override + public void setDisplayName(String displayName) throws DavException, IOException { + DavPropertySet updateProperties = new DavPropertySet(); + updateProperties.add(new DefaultDavProperty(DavPropertyName.DISPLAYNAME, displayName)); + + patchProperties(updateProperties, new DavPropertyNameSet()); + } + + @Override + public Optional getOwnerHref() throws PropertyParseException { + Optional ownerProp = getProperty(SecurityConstants.OWNER, ArrayList.class); + + if (ownerProp.isPresent()) { + for (Node child : (ArrayList) ownerProp.get()) { + if (child instanceof Element) { + String nameNode = child.getNodeName(); + if ((nameNode != null) && (DavConstants.XML_HREF.equals(nameNode))) + return Optional.of(child.getTextContent()); + } + } + } + + return Optional.absent(); + } + + @Override + public Optional getQuotaAvailableBytes() throws PropertyParseException { + return getProperty(WebDavConstants.PROPERTY_NAME_QUOTA_AVAILABLE_BYTES, Long.class); + } + + @Override + public Optional getQuotaUsedBytes() throws PropertyParseException { + return getProperty(WebDavConstants.PROPERTY_NAME_QUOTA_USED_BYTES, Long.class); + } + + @Override + public Optional getResourceId() throws PropertyParseException { + Optional resourceIdProp = getProperty(WebDavConstants.PROPERTY_NAME_RESOURCE_ID, ArrayList.class); + + if (resourceIdProp.isPresent()) { + for (Node child : (ArrayList) resourceIdProp.get()) { + if (child instanceof Element) { + String nameNode = child.getNodeName(); + if ((nameNode != null) && (DavConstants.XML_HREF.equals(nameNode))) + return Optional.of(child.getTextContent()); + } + } + } + + return Optional.absent(); + } + + // TODO: make use of this in REPORT methods if present. + @Override + public Optional getSyncToken() throws PropertyParseException { + return getProperty(WebDavConstants.PROPERTY_NAME_SYNC_TOKEN, String.class); + } + + public void fetchProperties(DavPropertyNameSet fetchProps) throws DavException, IOException { + PropFindMethod propFindMethod = new PropFindMethod(getPath(), fetchProps, PropFindMethod.DEPTH_0); + + try { + + getStore().getClient().execute(propFindMethod); + + if (propFindMethod.getStatusCode() == DavServletResponse.SC_MULTI_STATUS) { + MultiStatus multiStatus = propFindMethod.getResponseBodyAsMultiStatus(); + MultiStatusResponse[] responses = multiStatus.getResponses(); + + DavPropertySet foundProperties = responses[0].getProperties(DavServletResponse.SC_OK); + if (foundProperties != null) + properties = foundProperties; + } else + throw new DavException(propFindMethod.getStatusCode(), propFindMethod.getStatusText()); + + } finally { + propFindMethod.releaseConnection(); + } + } + + public void fetchProperties() throws DavException, IOException { + DavPropertyNameSet fetchProps = getPropertyNamesForFetch(); + PropFindMethod propFindMethod = new PropFindMethod(getPath(), fetchProps, PropFindMethod.DEPTH_0); + + try { + + getStore().getClient().execute(propFindMethod); + + if (propFindMethod.getStatusCode() == DavServletResponse.SC_MULTI_STATUS) { + MultiStatus multiStatus = propFindMethod.getResponseBodyAsMultiStatus(); + MultiStatusResponse[] responses = multiStatus.getResponses(); + + DavPropertySet foundProperties = responses[0].getProperties(DavServletResponse.SC_OK); + if (foundProperties != null) + properties = foundProperties; + } + else + throw new DavException(propFindMethod.getStatusCode(), propFindMethod.getStatusText()); + + } finally { + propFindMethod.releaseConnection(); + } + } + + @Override + public void patchProperties(DavPropertySet setProperties, DavPropertyNameSet removeProperties) + throws DavException, IOException + { + PropPatchMethod propPatchMethod = new PropPatchMethod(getPath(), setProperties, removeProperties); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + + try { + + propPatchMethod.getRequestEntity().writeRequest(stream); + getStore().getClient().execute(propPatchMethod); + + if (propPatchMethod.getStatusCode() != DavServletResponse.SC_MULTI_STATUS) + throw new DavException(propPatchMethod.getStatusCode(), propPatchMethod.getStatusText()); + + fetchProperties(); + + } finally { + propPatchMethod.releaseConnection(); + } + } + + @Override + public HashMap getComponentETags() throws DavException, IOException { + HashMap componentETagPairs = new HashMap(); + + DavPropertyNameSet fetchProps = new DavPropertyNameSet(); + fetchProps.add(DavPropertyName.GETETAG); + + PropFindMethod propFindMethod = new PropFindMethod(getPath(), fetchProps, PropFindMethod.DEPTH_1); + + try { + + getStore().getClient().execute(propFindMethod); + + if (propFindMethod.getStatusCode() == DavServletResponse.SC_MULTI_STATUS) { + MultiStatus multiStatus = propFindMethod.getResponseBodyAsMultiStatus(); + MultiStatusResponse[] responses = multiStatus.getResponses(); + + for (MultiStatusResponse msResponse : responses) { + DavPropertySet foundProperties = msResponse.getProperties(DavServletResponse.SC_OK); + Optional componentUid = getUidFromComponentPath(msResponse.getHref()); + + if (componentUid.isPresent() && foundProperties.get(DavPropertyName.PROPERTY_GETETAG) != null) { + DavProperty getETagProp = foundProperties.get(DavPropertyName.PROPERTY_GETETAG); + componentETagPairs.put(componentUid.get(), (String) getETagProp.getValue()); + } + } + } + else + throw new DavException(propFindMethod.getStatusCode(), + "PROPFIND response not multi-status, response: " + propFindMethod.getStatusLine()); + + } finally { + propFindMethod.releaseConnection(); + } + + return componentETagPairs; + } + + protected abstract List> getComponentsFromMultiStatus(MultiStatusResponse[] msResponses) + throws InvalidComponentException; + + @Override + public Optional> getComponent(String uid) + throws InvalidComponentException, DavException, IOException + { + ReportInfo reportInfo = new ReportInfo(getMultiGetReportType(), 1, getPropertyNamesForReports()); + + try { + + Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); + Element componentHREF = DomUtil.createElement(document, DavConstants.XML_HREF, DavConstants.NAMESPACE); + componentHREF.setTextContent(getComponentPathFromUid(uid)); + reportInfo.setContentElement(componentHREF); + + ReportMethod reportMethod = new ReportMethod(getPath(), reportInfo); + + try { + + getStore().getClient().execute(reportMethod); + if (reportMethod.getStatusCode() == DavServletResponse.SC_MULTI_STATUS) { + try { + + List> components = + getComponentsFromMultiStatus(reportMethod.getResponseBodyAsMultiStatus().getResponses()); + + if (components.size() == 0) + return Optional.absent(); + return Optional.of(components.get(0)); + + } catch (InvalidComponentException e) { + if (e.getCause() == null) + throw new InvalidComponentException(e.getMessage(), e.isServersFault(), e.getNamespace(), e.getPath(), uid); + + throw new InvalidComponentException(e.getMessage(), e.isServersFault(), e.getNamespace(), e.getPath(), uid, e.getCause()); + } + } else if (reportMethod.getStatusCode() == DavServletResponse.SC_NOT_FOUND) + return Optional.absent(); + else + throw new DavException(reportMethod.getStatusCode(), reportMethod.getStatusText()); + + } finally { + reportMethod.releaseConnection(); + } + + } catch (ParserConfigurationException e) { + throw new IOException("Caught exception while building document.", e); + } + } + + @Override + public List> getComponents() + throws InvalidComponentException, DavException, IOException + { + ReportInfo reportInfo = new ReportInfo(getQueryReportType(), 1, getPropertyNamesForReports()); + ReportMethod reportMethod = new ReportMethod(getPath(), reportInfo); + + try { + + getStore().getClient().execute(reportMethod); + + if (reportMethod.getStatusCode() == DavServletResponse.SC_MULTI_STATUS) + return getComponentsFromMultiStatus(reportMethod.getResponseBodyAsMultiStatus().getResponses()); + + throw new DavException(reportMethod.getStatusCode(), reportMethod.getStatusText()); + + } finally { + reportMethod.releaseConnection(); + } + } + + protected abstract void putComponentToServer(T calendar, Optional ifMatchETag) + throws DavException, IOException, InvalidComponentException; + + @Override + public void addComponent(T component) + throws InvalidComponentException, DavException, IOException + { + putComponentToServer(component, Optional.absent()); + fetchProperties(); + } + + @Override + public void updateComponent(ComponentETagPair component) + throws InvalidComponentException, DavException, IOException + { + putComponentToServer(component.getComponent(), component.getETag()); + fetchProperties(); + } + + @Override + public void removeComponent(String path) throws DavException, IOException { + DeleteMethod deleteMethod = new DeleteMethod(getComponentPathFromUid(path)); + + try { + + getStore().getClient().execute(deleteMethod); + if (!deleteMethod.succeeded()) + throw new DavException(deleteMethod.getStatusCode(), deleteMethod.getStatusText()); + + } finally { + deleteMethod.releaseConnection(); + } + + fetchProperties(); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/webdav/AbstractDavComponentStore.java b/flock/src/main/java/org/anhonesteffort/flock/webdav/AbstractDavComponentStore.java new file mode 100644 index 0000000..a8d5ce0 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/webdav/AbstractDavComponentStore.java @@ -0,0 +1,168 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.webdav; + +import com.google.common.base.Optional; + +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.Status; +import org.apache.jackrabbit.webdav.client.methods.PropFindMethod; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.w3c.dom.Element; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +/** + * Programmer: rhodey + */ +public abstract class AbstractDavComponentStore > + implements DavComponentStore +{ + protected final String productId = "OpenWhisperSystems - Flock"; + + protected final String hostHREF; + protected final String username; + protected final String password; + protected Optional currentUserPrincipal = Optional.absent(); + + private DavClient davClient; + private Optional> davOptions = Optional.absent(); + + public AbstractDavComponentStore(String hostHREF, + String username, + String password, + Optional currentUserPrincipal) + throws DavException, IOException + { + this.hostHREF = hostHREF; + this.username = username; + this.password = password; + this.currentUserPrincipal = currentUserPrincipal; + + this.davClient = new DavClient(new URL(hostHREF), username, password); + } + + public AbstractDavComponentStore(DavClient client, Optional currentUserPrincipal) { + this.davClient = client; + this.hostHREF = client.getDavHost().toString(); + this.username = client.getUsername(); + this.password = client.getPassword(); + this.currentUserPrincipal = currentUserPrincipal; + } + + public String getProductId() { + return productId; + } + + @Override + public String getHostHREF() { + return hostHREF; + } + + protected String getUserName() { + return username; + } + + protected String getPassword() { + return password; + } + + public DavClient getClient() { + return davClient; + } + + public List getDavOptions() throws DavException, IOException { + if (!davOptions.isPresent()) + davOptions = Optional.of(davClient.getDavOptions()); + + return davOptions.get(); + } + + public abstract Optional getCurrentUserPrincipal() throws IOException, DavException; + + protected Optional getCurrentUserPrincipal(String propFindUri) + throws IOException, DavException + { + DavPropertyNameSet props = new DavPropertyNameSet(); + props.add(WebDavConstants.PROPERTY_NAME_CURRENT_USER_PRINCIPAL); + + PropFindMethod propFindMethod = new PropFindMethod(propFindUri, + props, + PropFindMethod.DEPTH_0); + + try { + + getClient().execute(propFindMethod); + + MultiStatus multiStatus = propFindMethod.getResponseBodyAsMultiStatus(); + MultiStatusResponse[] msResponses = multiStatus.getResponses(); + + for (MultiStatusResponse msResponse : msResponses) { + DavPropertySet foundProperties = msResponse.getProperties(DavServletResponse.SC_OK); + DavProperty homeSetProperty = foundProperties.get(WebDavConstants.PROPERTY_NAME_CURRENT_USER_PRINCIPAL); + + for (Status status : msResponse.getStatus()) { + if (status.getStatusCode() == DavServletResponse.SC_OK) { + + if (homeSetProperty != null && homeSetProperty.getValue() instanceof ArrayList) { + for (Object child : (ArrayList) homeSetProperty.getValue()) { + if (child instanceof Element) { + String currentUserPrincipalUri = ((Element) child).getTextContent(); + if (!(currentUserPrincipalUri.endsWith("/"))) + currentUserPrincipalUri = currentUserPrincipalUri.concat("/"); + + return Optional.of(currentUserPrincipalUri); + } + } + } + + // Owncloud :( + else if (homeSetProperty != null && homeSetProperty.getValue() instanceof Element) { + String currentUserPrincipalUri = ((Element) homeSetProperty.getValue()).getTextContent(); + if (!(currentUserPrincipalUri.endsWith("/"))) + currentUserPrincipalUri = currentUserPrincipalUri.concat("/"); + + return Optional.of(currentUserPrincipalUri); + } + } + } + } + + } finally { + propFindMethod.releaseConnection(); + } + + return Optional.absent(); + } + + @Override + public void closeHttpConnection() { + getClient().closeHttpConnection(); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/webdav/ComponentETagPair.java b/flock/src/main/java/org/anhonesteffort/flock/webdav/ComponentETagPair.java new file mode 100644 index 0000000..3a0a689 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/webdav/ComponentETagPair.java @@ -0,0 +1,45 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.webdav; + +import com.google.common.base.Optional; + +/** + * Programmer: rhodey + */ +public class ComponentETagPair { + + private final T component; + private final Optional eTag; + + public ComponentETagPair(T component, Optional eTag) { + this.component = component; + this.eTag = eTag; + } + + public T getComponent() { + return component; + } + + public Optional getETag() { + return eTag; + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/webdav/DavClient.java b/flock/src/main/java/org/anhonesteffort/flock/webdav/DavClient.java new file mode 100644 index 0000000..1b3b893 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/webdav/DavClient.java @@ -0,0 +1,125 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.webdav; + +import org.apache.commons.httpclient.Credentials; +import org.apache.commons.httpclient.HostConfiguration; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpConnectionManager; +import org.apache.commons.httpclient.HttpMethodBase; +import org.apache.commons.httpclient.UsernamePasswordCredentials; +import org.apache.commons.httpclient.auth.AuthPolicy; +import org.apache.commons.httpclient.auth.AuthScope; +import org.apache.commons.httpclient.params.HttpClientParams; +import org.apache.commons.httpclient.protocol.Protocol; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.client.methods.OptionsMethod; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Programmer: rhodey + */ +public class DavClient { + + private static final int KEEP_ALIVE_TIMEOUT_SECONDS = 15; + + protected URL davHost; + protected String davUsername; + protected String davPassword; + + protected HttpClient client; + protected HostConfiguration hostConfiguration; + private HttpConnectionManager connectionManager; + + protected void initClient() { + HttpClientParams params = new HttpClientParams(); + List authPrefs = new ArrayList(2); + + authPrefs.add(AuthPolicy.DIGEST); + authPrefs.add(AuthPolicy.BASIC); + params.setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs); + params.setAuthenticationPreemptive(true); + + client = new HttpClient(params); + hostConfiguration = client.getHostConfiguration(); + connectionManager = client.getHttpConnectionManager(); + + hostConfiguration.setHost(davHost.getHost(), + davHost.getPort(), + Protocol.getProtocol(davHost.getProtocol())); + + Credentials credentials = new UsernamePasswordCredentials(davUsername, davPassword); + client.getState().setCredentials(AuthScope.ANY, credentials); + } + + public DavClient(URL davHost, String username, String password) { + this.davHost = davHost; + this.davUsername = username; + this.davPassword = password; + + initClient(); + } + + public URL getDavHost() { + return davHost; + } + + public String getUsername() { + return davUsername; + } + + public String getPassword() { + return davPassword; + } + + public List getDavOptions() throws IOException, DavException { + OptionsMethod optionsMethod = new OptionsMethod(davHost.toString()); + + try { + + execute(optionsMethod); + + if (optionsMethod.getStatusCode() >= 300) + throw new DavException(optionsMethod.getStatusCode(), + "Options method really shouldn't give us grief here... (" + optionsMethod.getStatusCode() + ")"); + + return Arrays.asList(optionsMethod.getAllowedMethods()); + + } finally { + optionsMethod.releaseConnection(); + } + } + + public int execute(HttpMethodBase method) throws IOException { + method.addRequestHeader("Connection", "Keep-Alive"); + method.addRequestHeader("Keep-Alive", "timeout=" + KEEP_ALIVE_TIMEOUT_SECONDS); + return client.executeMethod(hostConfiguration, method); + } + + protected void closeHttpConnection() { + connectionManager.closeIdleConnections(0); + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/webdav/DavComponentCollection.java b/flock/src/main/java/org/anhonesteffort/flock/webdav/DavComponentCollection.java new file mode 100644 index 0000000..2f2f152 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/webdav/DavComponentCollection.java @@ -0,0 +1,75 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.webdav; + +import com.google.common.base.Optional; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; + +/** + * Programmer: rhodey + */ +public interface DavComponentCollection { + + public String getPath(); + + public

Optional

getProperty(DavPropertyName propertyName, Class

type) + throws PropertyParseException; + + public Optional getCTag() throws PropertyParseException; + + public List getResourceTypes() throws PropertyParseException; + + public Optional getOwnerHref() throws PropertyParseException; + + public Optional getQuotaAvailableBytes() throws PropertyParseException; + + public Optional getQuotaUsedBytes() throws PropertyParseException; + + public Optional getResourceId() throws PropertyParseException; + + public Optional getSyncToken() throws PropertyParseException; + + public Optional getDisplayName() throws PropertyParseException; + + public void setDisplayName(String displayName) throws DavException, IOException; + + public void patchProperties(DavPropertySet setProperties, DavPropertyNameSet removeProperties) + throws DavException, IOException; + + public HashMap getComponentETags() throws DavException, IOException; + + public Optional> getComponent(String uid) throws InvalidComponentException, DavException, IOException; + + public List> getComponents() throws InvalidComponentException, DavException, IOException; + + public void addComponent(T component) throws InvalidComponentException, DavException, IOException; + + public void updateComponent(ComponentETagPair component) throws InvalidComponentException, DavException, IOException; + + public void removeComponent(String path) throws DavException, IOException; + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/webdav/DavComponentStore.java b/flock/src/main/java/org/anhonesteffort/flock/webdav/DavComponentStore.java new file mode 100644 index 0000000..b31014f --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/webdav/DavComponentStore.java @@ -0,0 +1,45 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.webdav; + +import com.google.common.base.Optional; +import org.apache.jackrabbit.webdav.DavException; + +import java.io.IOException; +import java.util.List; + +/** + * Programmer: rhodey + */ +public interface DavComponentStore> { + + public String getHostHREF(); + + public Optional getCollection(String path) throws DavException, IOException; + + public List getCollections() throws PropertyParseException, DavException, IOException; + + public void addCollection(String path) throws DavException, IOException; + + public void removeCollection(String path) throws DavException, IOException; + + public void closeHttpConnection(); + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/webdav/ExtendedMkCol.java b/flock/src/main/java/org/anhonesteffort/flock/webdav/ExtendedMkCol.java new file mode 100644 index 0000000..7db5a90 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/webdav/ExtendedMkCol.java @@ -0,0 +1,55 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.webdav; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.apache.jackrabbit.webdav.xml.XmlSerializable; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Programmer: rhodey + */ +public class ExtendedMkCol implements XmlSerializable { + + public static final String XML_MKCOL = "mkcol"; + + private final DavPropertySet properties; + + public ExtendedMkCol(DavPropertySet properties) { + this.properties = properties; + } + + public DavPropertySet getProperties() { + return properties; + } + + public Element toXml(Document document) { + Element mkCol = DomUtil.createElement(document, XML_MKCOL, DavConstants.NAMESPACE); + Element set = DomUtil.createElement(document, DavConstants.XML_SET, DavConstants.NAMESPACE); + + set.appendChild(properties.toXml(document)); + mkCol.appendChild(set); + + return mkCol; + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/webdav/InvalidComponentException.java b/flock/src/main/java/org/anhonesteffort/flock/webdav/InvalidComponentException.java new file mode 100644 index 0000000..8984fb6 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/webdav/InvalidComponentException.java @@ -0,0 +1,131 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.webdav; + +import com.google.common.base.Optional; +import org.apache.jackrabbit.webdav.xml.Namespace; + +/** + * Programmer: rhodey + */ +public class InvalidComponentException extends Exception { + + private boolean isServersFault; + private Namespace namespace; + private String path; + private String uid; + + public InvalidComponentException(String message, + boolean isServersFault, + Namespace namespace, + String path) + { + super(message); + + this.isServersFault = isServersFault; + this.namespace = namespace; + this.path = path; + } + + public InvalidComponentException(String message, + boolean isServersFault, + Namespace namespace, + String path, + String uid) + { + super(message); + + this.isServersFault = isServersFault; + this.namespace = namespace; + this.path = path; + this.uid = uid; + } + + public InvalidComponentException(String message, + boolean isServersFault, + Namespace namespace, + String path, + Throwable cause) + { + super(message, cause); + + this.isServersFault = isServersFault; + this.namespace = namespace; + this.path = path; + } + + public InvalidComponentException(String message, + boolean isServersFault, + Namespace namespace, + String path, + String uid, + Throwable cause) + { + super(message, cause); + + this.isServersFault = isServersFault; + this.namespace = namespace; + this.path = path; + this.uid = uid; + } + + public boolean isServersFault() { + return isServersFault; + } + + public Namespace getNamespace() { + return namespace; + } + + public String getPath() { + return path; + } + + public Optional getUid() { + return Optional.fromNullable(uid); + } + + @Override + public String toString() { + if (getCause() == null) { + if (!getUid().isPresent()) { + return "message: " + getMessage() + ", is servers fault: " + isServersFault + + ", namespace: " + namespace.getURI() + ", path: " + path; + } + else { + return "message: " + getMessage() + ", is servers fault: " + isServersFault + + ", namespace: " + namespace.getURI() + ", path: " + path + ", uid: " + getUid().get(); + } + } + else { + if (!getUid().isPresent()) { + return "message: " + getMessage() + ", is servers fault: " + isServersFault + + ", namespace: " + namespace.getURI() + ", path: " + path + ", cause: " + getCause(); + } + else { + return "message: " + getMessage() + ", is servers fault: " + isServersFault + + ", namespace: " + namespace.getURI() + ", path: " + path + + ", uid: " + getUid().get() + ", cause: " + getCause(); + } + } + + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/webdav/PropertyParseException.java b/flock/src/main/java/org/anhonesteffort/flock/webdav/PropertyParseException.java new file mode 100644 index 0000000..c4920a5 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/webdav/PropertyParseException.java @@ -0,0 +1,70 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.webdav; + +import org.apache.jackrabbit.webdav.property.DavPropertyName; + +/** + * Programmer: rhodey + */ +public class PropertyParseException extends Exception { + + private String path; + private DavPropertyName propertyName; + + + public PropertyParseException(String message, String path, DavPropertyName propertyName) { + super(message); + + this.path = path; + this.propertyName = propertyName; + } + + public PropertyParseException(String message, + String path, + DavPropertyName propertyName, + Throwable cause) + { + super(message, cause); + + this.path = path; + this.propertyName = propertyName; + } + + public String getPath() { + return path; + } + + public DavPropertyName getPropertyName() { + return propertyName; + } + + @Override + public String toString() { + if (getCause() == null) { + return "message: " + getMessage() + ", path: " + path + + ", property name: " + propertyName; + } + return "message: " + getMessage() + ", path: " + path + + ", property name: " + propertyName + ", cause: " + getCause(); + } + +} + diff --git a/flock/src/main/java/org/anhonesteffort/flock/webdav/WebDavConstants.java b/flock/src/main/java/org/anhonesteffort/flock/webdav/WebDavConstants.java new file mode 100644 index 0000000..1135d15 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/webdav/WebDavConstants.java @@ -0,0 +1,71 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.webdav; + +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.property.DavPropertyName; + +/** + * Programmer: rhodey + */ +public class WebDavConstants { + + public static final String PROPERTY_RESOURCE_ID = "resource-id"; + public static final String PROPERTY_CURRENT_USER_PRINCIPAL = "current-user-principal"; + public static final String PROPERTY_SUPPORTED_REPORT_SET = "supported-report-set"; + public static final String PROPERTY_SYNC_TOKEN = "sync-token"; + public static final String PROPERTY_QUOTA_AVAILABLE_BYTES = "quota-available-bytes"; + public static final String PROPERTY_QUOTA_USED_BYTES = "quota-used-bytes"; + + public static final DavPropertyName PROPERTY_NAME_PROP = DavPropertyName.create( + DavConstants.XML_PROP, + DavConstants.NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_RESOURCE_ID = DavPropertyName.create( + PROPERTY_RESOURCE_ID, + DavConstants.NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_CURRENT_USER_PRINCIPAL = DavPropertyName.create( + PROPERTY_CURRENT_USER_PRINCIPAL, + DavConstants.NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_SUPPORTED_REPORT_SET = DavPropertyName.create( + PROPERTY_SUPPORTED_REPORT_SET, + DavConstants.NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_SYNC_TOKEN = DavPropertyName.create( + PROPERTY_SYNC_TOKEN, + DavConstants.NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_QUOTA_USED_BYTES = DavPropertyName.create( + PROPERTY_QUOTA_USED_BYTES, + DavConstants.NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_QUOTA_AVAILABLE_BYTES = DavPropertyName.create( + PROPERTY_QUOTA_AVAILABLE_BYTES, + DavConstants.NAMESPACE + ); +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/webdav/caldav/CalDavCollection.java b/flock/src/main/java/org/anhonesteffort/flock/webdav/caldav/CalDavCollection.java new file mode 100644 index 0000000..4c99989 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/webdav/caldav/CalDavCollection.java @@ -0,0 +1,403 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.webdav.caldav; + +import com.google.common.base.Optional; + +import net.fortuna.ical4j.data.CalendarBuilder; +import net.fortuna.ical4j.data.CalendarOutputter; +import net.fortuna.ical4j.data.ParserException; +import net.fortuna.ical4j.model.Calendar; +import net.fortuna.ical4j.model.Component; +import net.fortuna.ical4j.model.ConstraintViolationException; +import net.fortuna.ical4j.model.ValidationException; +import net.fortuna.ical4j.model.property.ProdId; +import net.fortuna.ical4j.util.Calendars; +import org.anhonesteffort.flock.webdav.AbstractDavComponentCollection; +import org.anhonesteffort.flock.webdav.ComponentETagPair; +import org.anhonesteffort.flock.webdav.InvalidComponentException; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.commons.httpclient.methods.ByteArrayRequestEntity; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.client.methods.PutMethod; +import org.apache.jackrabbit.webdav.client.methods.ReportMethod; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.security.report.PrincipalMatchReport; +import org.apache.jackrabbit.webdav.version.report.ReportInfo; +import org.apache.jackrabbit.webdav.version.report.ReportType; +import org.apache.jackrabbit.webdav.xml.DomUtil; +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +/** + * Programmer: rhodey + */ +public class CalDavCollection extends AbstractDavComponentCollection implements DavCalendarCollection { + + public CalDavCollection(CalDavStore calDavStore, String path) { + super(calDavStore, path); + } + + public CalDavCollection(CalDavStore calDavStore, + String path, + String displayName, + String description, + Integer color) + { + super(calDavStore, path, displayName); + properties.add(new DefaultDavProperty(CalDavConstants.PROPERTY_NAME_CALENDAR_DESCRIPTION, description)); + properties.add(new DefaultDavProperty(CalDavConstants.PROPERTY_NAME_CALENDAR_COLOR, color)); + } + + public CalDavCollection(CalDavStore calDavStore, String path, DavPropertySet properties) { + super(calDavStore, path, properties); + this.properties = properties; + } + + @Override + protected String getComponentPathFromUid(String uid) { + return getPath().concat(uid.concat(CalDavConstants.ICAL_FILE_EXTENSION)); + } + + @Override + protected DavPropertyNameSet getPropertyNamesForFetch() { + DavPropertyNameSet calendarProps = super.getPropertyNamesForFetch(); + + calendarProps.add(CalDavConstants.PROPERTY_NAME_CALENDAR_DESCRIPTION); + calendarProps.add(CalDavConstants.PROPERTY_NAME_SUPPORTED_CALENDAR_COMPONENT_SET); + calendarProps.add(CalDavConstants.PROPERTY_NAME_CALENDAR_TIMEZONE); + calendarProps.add(CalDavConstants.PROPERTY_NAME_SUPPORTED_CALENDAR_DATA); + calendarProps.add(CalDavConstants.PROPERTY_NAME_MAX_ATTENDEES_PER_INSTANCE); + calendarProps.add(CalDavConstants.PROPERTY_NAME_MAX_DATE_TIME); + calendarProps.add(CalDavConstants.PROPERTY_NAME_MIN_DATE_TIME); + calendarProps.add(CalDavConstants.PROPERTY_NAME_MAX_INSTANCES); + calendarProps.add(CalDavConstants.PROPERTY_NAME_MAX_RESOURCE_SIZE); + + calendarProps.add(CalDavConstants.PROPERTY_NAME_CALENDAR_COLOR); + calendarProps.add(CalDavConstants.PROPERTY_NAME_CALENDAR_ORDER); + + return calendarProps; + } + + @Override + protected ReportType getQueryReportType() { + return ReportType.register(CalDavConstants.PROPERTY_CALENDAR_QUERY, + CalDavConstants.CALDAV_NAMESPACE, + PrincipalMatchReport.class); + } + + @Override + protected ReportType getMultiGetReportType() { + return ReportType.register(CalDavConstants.PROPERTY_CALENDAR_MULTIGET, + CalDavConstants.CALDAV_NAMESPACE, + PrincipalMatchReport.class); + } + + @Override + protected DavPropertyNameSet getPropertyNamesForReports() { + DavPropertyNameSet calendarProperties = new DavPropertyNameSet(); + calendarProperties.add(CalDavConstants.PROPERTY_NAME_CALENDAR_DATA); + calendarProperties.add(DavPropertyName.GETETAG); + return calendarProperties; + } + + @Override + public Optional getDescription() throws PropertyParseException { + return getProperty(CalDavConstants.PROPERTY_NAME_CALENDAR_DESCRIPTION, String.class); + } + + @Override + public void setDescription(String description) throws DavException, IOException { + DavPropertySet updateProperties = new DavPropertySet(); + updateProperties.add(new DefaultDavProperty(CalDavConstants.PROPERTY_NAME_CALENDAR_DESCRIPTION, description)); + + patchProperties(updateProperties, new DavPropertyNameSet()); + } + + @Override + public Optional getTimeZone() throws PropertyParseException { + try { + + Optional calendarTimeZone = getProperty(CalDavConstants.PROPERTY_NAME_CALENDAR_TIMEZONE, String.class); + if (calendarTimeZone.isPresent()) + return Optional.of(new CalendarBuilder().build(new StringReader(calendarTimeZone.get()))); + + } catch (IOException e) { + throw new PropertyParseException("caught exception while building time zone.", + getPath(), CalDavConstants.PROPERTY_NAME_CALENDAR_TIMEZONE, e); + } catch (ParserException e) { + throw new PropertyParseException("caught exception while building time zone.", + getPath(), CalDavConstants.PROPERTY_NAME_CALENDAR_TIMEZONE, e); + } + return Optional.absent(); + } + + @Override + public void setTimeZone(Calendar timezone) throws DavException, IOException { + DavPropertySet updateProperties = new DavPropertySet(); + timezone.getProperties().add(new ProdId(((CalDavStore)getStore()).getProductId())); + updateProperties.add(new DefaultDavProperty(CalDavConstants.PROPERTY_NAME_CALENDAR_TIMEZONE, timezone.toString())); + + patchProperties(updateProperties, new DavPropertyNameSet()); + } + + @Override + public List getSupportedComponentSet() throws PropertyParseException { + List supportedComponents = new ArrayList(); + Optional supportedCalCompSetProp = + getProperty(CalDavConstants.PROPERTY_NAME_SUPPORTED_CALENDAR_COMPONENT_SET, ArrayList.class); + + if (supportedCalCompSetProp.isPresent()) { + for (Node child : (ArrayList) supportedCalCompSetProp.get()) { + if (child instanceof Element) { + Node nameNode = child.getAttributes().getNamedItem(CalDavConstants.ATTRIBUTE_NAME); + if (nameNode != null) + supportedComponents.add(nameNode.getTextContent()); + } + } + } + + return supportedComponents; + } + + @Override + public Optional getMaxResourceSize() throws PropertyParseException { + return getProperty(CalDavConstants.PROPERTY_NAME_MAX_RESOURCE_SIZE, Long.class); + } + + @Override + public Optional getMinDateTime() throws PropertyParseException { + return getProperty(CalDavConstants.PROPERTY_NAME_MIN_DATE_TIME, String.class); + } + + @Override + public Optional getMaxDateTime() throws PropertyParseException { + return getProperty(CalDavConstants.PROPERTY_NAME_MAX_DATE_TIME, String.class); + } + + @Override + public Optional getMaxInstances() throws PropertyParseException { + return getProperty(CalDavConstants.PROPERTY_NAME_MAX_INSTANCES, Integer.class); + } + + @Override + public Optional getMaxAttendeesPerInstance() throws PropertyParseException { + return getProperty(CalDavConstants.PROPERTY_NAME_MAX_ATTENDEES_PER_INSTANCE, Integer.class); + } + + @Override + public Optional getColor() throws PropertyParseException { + return getProperty(CalDavConstants.PROPERTY_NAME_CALENDAR_COLOR, Integer.class); + } + + @Override + public void setColor(int color) throws DavException, IOException { + DavPropertySet updateProperties = new DavPropertySet(); + updateProperties.add(new DefaultDavProperty(CalDavConstants.PROPERTY_NAME_CALENDAR_COLOR, color)); + + patchProperties(updateProperties, new DavPropertyNameSet()); + } + + @Override + public Optional getOrder() throws PropertyParseException { + return getProperty(CalDavConstants.PROPERTY_NAME_CALENDAR_ORDER, Integer.class); + } + + @Override + public void setOrder(Integer order) throws DavException, IOException { + DavPropertySet updateProperties = new DavPropertySet(); + updateProperties.add(new DefaultDavProperty(CalDavConstants.PROPERTY_NAME_CALENDAR_ORDER, order)); + + patchProperties(updateProperties, new DavPropertyNameSet()); + } + + @Override + protected List> getComponentsFromMultiStatus(MultiStatusResponse[] msResponses) + throws InvalidComponentException + { + List> calendars = new LinkedList>(); + + try { + + for (MultiStatusResponse response : msResponses) { + Calendar calendar = null; + String eTag = null; + DavPropertySet propertySet = response.getProperties(DavServletResponse.SC_OK); + + if (propertySet.get(CalDavConstants.PROPERTY_NAME_CALENDAR_DATA) != null) { + String calendarData = (String) propertySet.get(CalDavConstants.PROPERTY_NAME_CALENDAR_DATA).getValue(); + + // OwnCloud :( + if (!calendarData.contains("\r")) + calendarData = calendarData.replace("\n", "\r\n"); + + calendar = new CalendarBuilder().build(new StringReader(calendarData)); + } + + if (propertySet.get(DavPropertyName.GETETAG) != null) + eTag = (String) propertySet.get(DavPropertyName.GETETAG).getValue(); + + if (calendar != null) + calendars.add(new ComponentETagPair(calendar, Optional.fromNullable(eTag))); + } + + } catch (IOException e) { + throw new InvalidComponentException("Caught exception while parsing MultiStatus", false, + CalDavConstants.CALDAV_NAMESPACE, getPath(), e); + } catch (ParserException e) { + throw new InvalidComponentException("Caught exception while parsing calendar data", true, + CalDavConstants.CALDAV_NAMESPACE, getPath(), e); + } + + return calendars; + } + + private List> getComponentsByType(String componentType) + throws InvalidComponentException, DavException, IOException + { + try { + Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); + Element resourceFilter = DomUtil.createElement(document, + CalDavConstants.PROPERTY_FILTER, + CalDavConstants.CALDAV_NAMESPACE); + Element calendarFilter = DomUtil.createElement(document, + CalDavConstants.PROPERTY_COMP_FILTER, + CalDavConstants.CALDAV_NAMESPACE); + Element componentFilter = DomUtil.createElement(document, + CalDavConstants.PROPERTY_COMP_FILTER, + CalDavConstants.CALDAV_NAMESPACE); + + componentFilter.setAttribute(CalDavConstants.ATTRIBUTE_NAME, componentType); + calendarFilter.setAttribute(CalDavConstants.ATTRIBUTE_NAME, Calendar.VCALENDAR); + calendarFilter.appendChild(componentFilter); + resourceFilter.appendChild(calendarFilter); + + ReportInfo reportInfo = new ReportInfo(getQueryReportType(), 1, getPropertyNamesForReports()); + reportInfo.setContentElement(resourceFilter); + + ReportMethod reportMethod = new ReportMethod(getPath(), reportInfo); + + try { + + getStore().getClient().execute(reportMethod); + + if (reportMethod.getStatusCode() == DavServletResponse.SC_MULTI_STATUS) + return getComponentsFromMultiStatus(reportMethod.getResponseBodyAsMultiStatus().getResponses()); + + throw new DavException(reportMethod.getStatusCode(), reportMethod.getStatusText()); + + } finally { + reportMethod.releaseConnection(); + } + + } catch (DOMException e) { + throw new InvalidComponentException("Caught exception while parsing DOM", false, + CalDavConstants.CALDAV_NAMESPACE, getPath(), e); + } catch (ParserConfigurationException e) { + throw new IOException("Caught exception while building document.", e); + } + } + + public List> getEventComponents() + throws InvalidComponentException, DavException, IOException + { + return getComponentsByType(Component.VEVENT); + } + + public List> getToDoComponents() + throws InvalidComponentException, DavException, IOException + { + return getComponentsByType(Component.VTODO); + } + + @Override + protected void putComponentToServer(Calendar calendar, Optional ifMatchETag) + throws InvalidComponentException, DavException, IOException + { + calendar.getProperties().remove(ProdId.PRODID); + calendar.getProperties().add(new ProdId(((CalDavStore)getStore()).getProductId())); + + try { + + if (Calendars.getUid(calendar) == null) + throw new InvalidComponentException("Cannot put iCal to server without UID!", false, + CalDavConstants.CALDAV_NAMESPACE, getPath()); + + String calendarUid = Calendars.getUid(calendar).getValue(); + PutMethod putMethod = new PutMethod(getComponentPathFromUid(calendarUid)); + + if (ifMatchETag.isPresent()) + putMethod.addRequestHeader("If-Match", ifMatchETag.get()); // TODO: constant for this. + else + putMethod.addRequestHeader("If-None-Match", "*"); // TODO: constant for this. + + try { + + CalendarOutputter calendarOutputter = new CalendarOutputter(); + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + + calendarOutputter.output(calendar, byteStream); + putMethod.setRequestEntity(new ByteArrayRequestEntity(byteStream.toByteArray(), CalDavConstants.HEADER_CONTENT_TYPE_CALENDAR)); + + getStore().getClient().execute(putMethod); + int status = putMethod.getStatusCode(); + + if (status == DavServletResponse.SC_REQUEST_ENTITY_TOO_LARGE || + status == DavServletResponse.SC_FORBIDDEN) + { + throw new InvalidComponentException("Put method returned bad status " + status, false, + CalDavConstants.CALDAV_NAMESPACE, getPath()); + } + + if (status < DavServletResponse.SC_OK || + status > DavServletResponse.SC_NO_CONTENT) + { + throw new DavException(status, putMethod.getStatusText()); + } + + } finally { + putMethod.releaseConnection(); + } + + } catch (ConstraintViolationException e) { + throw new InvalidComponentException("Caught exception while parsing UID from calendar", false, + CalDavConstants.CALDAV_NAMESPACE, getPath(), e); + } catch (ValidationException e) { + throw new InvalidComponentException("Caught exception whie outputting calendar to stream", false, + CalDavConstants.CALDAV_NAMESPACE, getPath(), e); + } + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/webdav/caldav/CalDavConstants.java b/flock/src/main/java/org/anhonesteffort/flock/webdav/caldav/CalDavConstants.java new file mode 100644 index 0000000..3a87f2c --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/webdav/caldav/CalDavConstants.java @@ -0,0 +1,132 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ +package org.anhonesteffort.flock.webdav.caldav; + +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.xml.Namespace; + +import java.util.HashSet; +import java.util.Set; + +/** + * Programmer: rhodey + */ +public class CalDavConstants { + + public static final Namespace CALDAV_NAMESPACE = Namespace.getNamespace("C", "urn:ietf:params:xml:ns:caldav"); + public static final Namespace CALENDAR_SERVER_NAMESPACE = Namespace.getNamespace("S", "http://calendarserver.org/ns/"); + public static final Namespace ICAL_NAMESPACE = Namespace.getNamespace("I", "http://apple.com/ns/ical/"); + + public static final String RESOURCE_TYPE_CALENDAR = "calendar"; + public static final String RESOURCE_TYPE_CALENDAR_PROXY_READ = "calendar-proxy-read"; + public static final String RESOURCE_TYPE_CALENDAR_PROXY_WRITE = "calendar-proxy-write"; + + public static final String HEADER_CONTENT_TYPE_CALENDAR = "text/calendar"; + public static final String ICAL_FILE_EXTENSION = ".ics"; + public static final String ATTRIBUTE_NAME = "name"; + + public static final String PROPERTY_CTAG = "getctag"; + public static final String PROPERTY_CALENDAR_DESCRIPTION = "calendar-description"; + public static final String PROPERTY_CALENDAR_COLOR = "calendar-color"; + public static final String PROPERTY_CALENDAR_ORDER = "calendar-order"; + public static final String PROPERTY_CALENDAR_HOME_SET = "calendar-home-set"; + public static final String PROPERTY_SUPPORTED_CALENDAR_COMPONENT_SET = "supported-calendar-component-set"; + public static final String PROPERTY_CALENDAR_TIMEZONE = "calendar-timezone"; + public static final String PROPERTY_SUPPORTED_CALENDAR_DATA = "supported-calendar-data"; + public static final String PROPERTY_MAX_RESOURCE_SIZE = "max-resource-size"; + public static final String PROPERTY_MIN_DATE_TIME = "min-date-time"; + public static final String PROPERTY_MAX_DATE_TIME = "max-date-time"; + public static final String PROPERTY_MAX_INSTANCES = "max-instances"; + public static final String PROPERTY_MAX_ATTENDEES_PER_INSTANCE = "max-attendees-per-instance"; + public static final String PROPERTY_CALENDAR_DATA = "calendar-data"; + public static final String PROPERTY_CALENDAR_QUERY = "calendar-query"; + public static final String PROPERTY_CALENDAR_MULTIGET = "calendar-multiget"; + public static final String PROPERTY_FILTER = "filter"; + public static final String PROPERTY_COMP_FILTER = "comp-filter"; + + public static final DavPropertyName PROPERTY_NAME_CALENDAR_DESCRIPTION = DavPropertyName.create( + PROPERTY_CALENDAR_DESCRIPTION, + CALDAV_NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_CALENDAR_TIMEZONE = DavPropertyName.create( + PROPERTY_CALENDAR_TIMEZONE, + CALDAV_NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_SUPPORTED_CALENDAR_COMPONENT_SET = DavPropertyName.create( + PROPERTY_SUPPORTED_CALENDAR_COMPONENT_SET, + CALDAV_NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_SUPPORTED_CALENDAR_DATA = DavPropertyName.create( + PROPERTY_SUPPORTED_CALENDAR_DATA, + CALDAV_NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_MAX_RESOURCE_SIZE = DavPropertyName.create( + PROPERTY_MAX_RESOURCE_SIZE, + CALDAV_NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_MIN_DATE_TIME = DavPropertyName.create( + PROPERTY_MIN_DATE_TIME, + CALDAV_NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_MAX_DATE_TIME = DavPropertyName.create( + PROPERTY_MAX_DATE_TIME, + CALDAV_NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_MAX_INSTANCES = DavPropertyName.create( + PROPERTY_MAX_INSTANCES, + CALDAV_NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_MAX_ATTENDEES_PER_INSTANCE = DavPropertyName.create( + PROPERTY_MAX_ATTENDEES_PER_INSTANCE, + CALDAV_NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_CALENDAR_DATA = DavPropertyName.create( + PROPERTY_CALENDAR_DATA, + CALDAV_NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_CALENDAR_HOME_SET = DavPropertyName.create( + PROPERTY_CALENDAR_HOME_SET, + CALDAV_NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_CTAG = DavPropertyName.create( + CalDavConstants.PROPERTY_CTAG, + CalDavConstants.CALENDAR_SERVER_NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_CALENDAR_COLOR = DavPropertyName.create( + CalDavConstants.PROPERTY_CALENDAR_COLOR, + CalDavConstants.ICAL_NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_CALENDAR_ORDER = DavPropertyName.create( + CalDavConstants.PROPERTY_CALENDAR_ORDER, + CalDavConstants.ICAL_NAMESPACE + ); +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/webdav/caldav/CalDavStore.java b/flock/src/main/java/org/anhonesteffort/flock/webdav/caldav/CalDavStore.java new file mode 100644 index 0000000..47cd033 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/webdav/caldav/CalDavStore.java @@ -0,0 +1,352 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.webdav.caldav; + +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.webdav.WebDavConstants; + +import org.anhonesteffort.flock.webdav.AbstractDavComponentStore; +import org.anhonesteffort.flock.webdav.DavClient; +import org.anhonesteffort.flock.webdav.DavComponentStore; +import org.anhonesteffort.flock.webdav.ExtendedMkCol; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.commons.httpclient.Header; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.Status; +import org.apache.jackrabbit.webdav.client.methods.DeleteMethod; +import org.apache.jackrabbit.webdav.client.methods.MkColMethod; +import org.apache.jackrabbit.webdav.client.methods.PropFindMethod; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +/** + * Programmer: rhodey + */ +public class CalDavStore extends AbstractDavComponentStore + implements DavComponentStore +{ + protected Optional calendarHomeSet = Optional.absent(); + + public CalDavStore(String hostHREF, + String username, + String password, + Optional currentUserPrincipal, + Optional calendarHomeSet) + throws DavException, IOException + { + super(hostHREF, username, password, currentUserPrincipal); + this.calendarHomeSet = calendarHomeSet; + } + + public CalDavStore(DavClient client, + Optional currentUserPrincipal, + Optional calendarHomeSet) + { + super(client, currentUserPrincipal); + this.calendarHomeSet = calendarHomeSet; + } + + @Override + public Optional getCurrentUserPrincipal() throws DavException, IOException { + if (currentUserPrincipal.isPresent()) + return currentUserPrincipal; + + DavPropertyNameSet props = new DavPropertyNameSet(); + props.add(WebDavConstants.PROPERTY_NAME_CURRENT_USER_PRINCIPAL); + + String propFindUri = getHostHREF().concat("/.well-known/caldav"); + PropFindMethod propFindMethod = new PropFindMethod(propFindUri, + props, + PropFindMethod.DEPTH_0); + + try { + + getClient().execute(propFindMethod); + propFindMethod.getResponseBodyAsMultiStatus(); + + } catch (DavException e) { + + if (e.getErrorCode() == DavServletResponse.SC_MOVED_PERMANENTLY) { + Header locationHeader = propFindMethod.getResponseHeader("location"); // TODO: find constant for this... + if (locationHeader.getValue() != null) { + currentUserPrincipal = super.getCurrentUserPrincipal(locationHeader.getValue()); + return currentUserPrincipal; + } + } + throw e; + + } finally { + propFindMethod.releaseConnection(); + } + + return Optional.absent(); + } + + public Optional getCalendarHomeSet() + throws PropertyParseException, DavException, IOException + { + if (calendarHomeSet.isPresent()) + return calendarHomeSet; + + if (!getCurrentUserPrincipal().isPresent()) + throw new PropertyParseException("DAV:current-user-principal unavailable, server must support rfc5785.", + getHostHREF(), WebDavConstants.PROPERTY_NAME_CURRENT_USER_PRINCIPAL); + + DavPropertyNameSet principalsProps = new DavPropertyNameSet(); + principalsProps.add(CalDavConstants.PROPERTY_NAME_CALENDAR_HOME_SET); + principalsProps.add(DavPropertyName.DISPLAYNAME); + + String propFindUri = getHostHREF().concat(getCurrentUserPrincipal().get()); + PropFindMethod propFindMethod = new PropFindMethod(propFindUri, + principalsProps, + PropFindMethod.DEPTH_0); + + try { + + getClient().execute(propFindMethod); + + MultiStatus multiStatus = propFindMethod.getResponseBodyAsMultiStatus(); + MultiStatusResponse[] msResponses = multiStatus.getResponses(); + + for (MultiStatusResponse msResponse : msResponses) { + DavPropertySet foundProperties = msResponse.getProperties(DavServletResponse.SC_OK); + DavProperty homeSetProperty = foundProperties.get(CalDavConstants.PROPERTY_NAME_CALENDAR_HOME_SET); + + for (Status status : msResponse.getStatus()) { + if (status.getStatusCode() == DavServletResponse.SC_OK) { + + if (homeSetProperty != null && homeSetProperty.getValue() instanceof ArrayList) { + for (Object child : (ArrayList) homeSetProperty.getValue()) { + if (child instanceof Element) { + String calendarHomeSetUri = ((Element) child).getTextContent(); + if (!(calendarHomeSetUri.endsWith("/"))) + calendarHomeSetUri = calendarHomeSetUri.concat("/"); + + calendarHomeSet = Optional.of(calendarHomeSetUri); + return calendarHomeSet; + } + } + } + + // OwnCloud :( + else if (homeSetProperty != null && homeSetProperty.getValue() instanceof Element) { + String calendarHomeSetUri = ((Element) homeSetProperty.getValue()).getTextContent(); + if (!(calendarHomeSetUri.endsWith("/"))) + calendarHomeSetUri = calendarHomeSetUri.concat("/"); + + calendarHomeSet = Optional.of(calendarHomeSetUri); + return calendarHomeSet; + } + } + } + } + + } finally { + propFindMethod.releaseConnection(); + } + + return Optional.absent(); + } + + @Override + public void addCollection(String path) + throws DavException, IOException + { + addCollection(path, new DavPropertySet()); + } + + public void addCollection(String path, DavPropertySet properties) + throws DavException, IOException + { + ArrayList resourceTypes = new ArrayList(); + resourceTypes.add(DavPropertyName.create(DavConstants.XML_COLLECTION, DavConstants.NAMESPACE)); + resourceTypes.add(DavPropertyName.create("calendar", CalDavConstants.CALDAV_NAMESPACE)); // TODO: constant for this... + properties.add(new DefaultDavProperty>(DavPropertyName.RESOURCETYPE, resourceTypes)); + + MkColMethod mkColMethod = new MkColMethod(getHostHREF().concat(path)); + ExtendedMkCol extendedMkCol = new ExtendedMkCol(properties); + + try { + + mkColMethod.setRequestBody(extendedMkCol); + getClient().execute(mkColMethod); + + if (!mkColMethod.succeeded()) + throw new DavException(mkColMethod.getStatusCode(), mkColMethod.getStatusText()); + + } finally { + mkColMethod.releaseConnection(); + } + } + + public void addCollection(String path, + String displayName, + String description, + int color) + throws DavException, IOException + { + DavPropertySet properties = new DavPropertySet(); + properties.add(new DefaultDavProperty( DavPropertyName.DISPLAYNAME, displayName)); + properties.add(new DefaultDavProperty( CalDavConstants.PROPERTY_NAME_CALENDAR_DESCRIPTION, description)); + properties.add(new DefaultDavProperty(CalDavConstants.PROPERTY_NAME_CALENDAR_COLOR, color)); + addCollection(path, properties); + } + + // TODO: I don't like this... + public static List getCollectionsFromMultiStatusResponses(CalDavStore store, + MultiStatusResponse[] msResponses) + { + List collections = new LinkedList(); + + for (MultiStatusResponse msResponse : msResponses) { + DavPropertySet foundProperties = msResponse.getProperties(DavServletResponse.SC_OK); + String collectionUri = msResponse.getHref(); + + for (Status status : msResponse.getStatus()) { + if (status.getStatusCode() == DavServletResponse.SC_OK) { + + boolean isCalendarCollection = false; + DavPropertySet collectionProperties = new DavPropertySet(); + + DavProperty resourceTypeProperty = foundProperties.get(DavPropertyName.RESOURCETYPE); + if (resourceTypeProperty != null) { + + Object resourceTypeValue = resourceTypeProperty.getValue(); + if (resourceTypeValue instanceof ArrayList) { + for (Node child : (ArrayList) resourceTypeValue) { + if (child instanceof Element) { + String localName = child.getLocalName(); + if (localName != null) { + isCalendarCollection = localName.equals(CalDavConstants.RESOURCE_TYPE_CALENDAR) || + localName.equals(CalDavConstants.RESOURCE_TYPE_CALENDAR_PROXY_READ) || + localName.equals(CalDavConstants.RESOURCE_TYPE_CALENDAR_PROXY_WRITE); + } + } + } + } + } + + if (isCalendarCollection) { + for (DavProperty property : foundProperties) { + if (property != null) + collectionProperties.add(property); + } + collections.add(new CalDavCollection(store, collectionUri, collectionProperties)); + } + + } + } + } + + return collections; + } + + @Override + public Optional getCollection(String path) throws DavException, IOException { + CalDavCollection targetCollection = new CalDavCollection(this, path); + DavPropertyNameSet collectionProps = targetCollection.getPropertyNamesForFetch(); + PropFindMethod propFindMethod = new PropFindMethod(path, collectionProps, PropFindMethod.DEPTH_0); + + try { + + getClient().execute(propFindMethod); + + MultiStatus multiStatus = propFindMethod.getResponseBodyAsMultiStatus(); + MultiStatusResponse[] responses = multiStatus.getResponses(); + + List returnedCollections = getCollectionsFromMultiStatusResponses(this, responses); + + if (returnedCollections.size() == 0) + Optional.absent(); + + return Optional.of(returnedCollections.get(0)); + + } catch (DavException e) { + if (e.getErrorCode() == DavServletResponse.SC_NOT_FOUND) + return Optional.absent(); + + throw e; + } finally { + propFindMethod.releaseConnection(); + } + } + + @Override + public List getCollections() + throws PropertyParseException, DavException, IOException + { + Optional calHomeSetUri = getCalendarHomeSet(); + if (!calHomeSetUri.isPresent()) + throw new PropertyParseException("No calendar-home-set property found for user.", + getHostHREF(), CalDavConstants.PROPERTY_NAME_CALENDAR_HOME_SET); + + CalDavCollection hack = new CalDavCollection(this, ""); + DavPropertyNameSet calendarProps = hack.getPropertyNamesForFetch(); + + PropFindMethod method = new PropFindMethod(getHostHREF().concat(calHomeSetUri.get()), + calendarProps, + PropFindMethod.DEPTH_1); + + try { + + getClient().execute(method); + + MultiStatus multiStatus = method.getResponseBodyAsMultiStatus(); + MultiStatusResponse[] responses = multiStatus.getResponses(); + + return getCollectionsFromMultiStatusResponses(this, responses); + + } finally { + method.releaseConnection(); + } + } + + @Override + public void removeCollection(String path) throws DavException, IOException { + DeleteMethod deleteMethod = new DeleteMethod(getHostHREF().concat(path)); + + try { + + getClient().execute(deleteMethod); + + if (!deleteMethod.succeeded()) + throw new DavException(deleteMethod.getStatusCode(), deleteMethod.getStatusText()); + + } finally { + deleteMethod.releaseConnection(); + } + } + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/webdav/caldav/DavCalendarCollection.java b/flock/src/main/java/org/anhonesteffort/flock/webdav/caldav/DavCalendarCollection.java new file mode 100644 index 0000000..15698cc --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/webdav/caldav/DavCalendarCollection.java @@ -0,0 +1,65 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.webdav.caldav; + +import com.google.common.base.Optional; + +import net.fortuna.ical4j.model.Calendar; +import org.anhonesteffort.flock.webdav.DavComponentCollection; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.jackrabbit.webdav.DavException; + +import java.io.IOException; +import java.util.List; + +/** + * Programmer: rhodey + */ +public interface DavCalendarCollection extends DavComponentCollection { + + public Optional getDescription() throws PropertyParseException; + + public void setDescription(String description) throws IOException, DavException; + + public Optional getTimeZone() throws PropertyParseException; + + public void setTimeZone(Calendar timezone) throws IOException, DavException; + + public List getSupportedComponentSet() throws PropertyParseException; + + public Optional getMaxResourceSize() throws PropertyParseException; + + public Optional getMinDateTime() throws PropertyParseException; + + public Optional getMaxDateTime() throws PropertyParseException; + + public Optional getMaxInstances() throws PropertyParseException; + + public Optional getMaxAttendeesPerInstance() throws PropertyParseException; + + public Optional getColor() throws PropertyParseException; + + public void setColor(int color) throws IOException, DavException; + + public Optional getOrder() throws PropertyParseException; + + public void setOrder(Integer order) throws IOException, DavException; + +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/webdav/carddav/CardDavCollection.java b/flock/src/main/java/org/anhonesteffort/flock/webdav/carddav/CardDavCollection.java new file mode 100644 index 0000000..d231c57 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/webdav/carddav/CardDavCollection.java @@ -0,0 +1,207 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.webdav.carddav; + +import android.util.Log; + +import com.google.common.base.Optional; +import ezvcard.Ezvcard; +import ezvcard.VCard; +import ezvcard.property.ProductId; + +import org.anhonesteffort.flock.webdav.AbstractDavComponentCollection; +import org.anhonesteffort.flock.webdav.ComponentETagPair; +import org.anhonesteffort.flock.webdav.InvalidComponentException; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.commons.httpclient.methods.ByteArrayRequestEntity; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.client.methods.PutMethod; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.apache.jackrabbit.webdav.security.SecurityConstants; +import org.apache.jackrabbit.webdav.security.report.PrincipalMatchReport; +import org.apache.jackrabbit.webdav.version.report.ReportType; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +/** + * Programmer: rhodey + */ +public class CardDavCollection extends AbstractDavComponentCollection implements DavContactCollection { + + protected CardDavCollection(CardDavStore cardDavStore, String path) { + super(cardDavStore, path); + } + + protected CardDavCollection(CardDavStore cardDavStore, + String path, + String displayName, + String description) + { + super(cardDavStore, path, displayName); + properties.add(new DefaultDavProperty(CardDavConstants.PROPERTY_NAME_ADDRESSBOOK_DESCRIPTION, description)); + } + + protected CardDavCollection(CardDavStore cardDavStore, String path, DavPropertySet properties) { + super(cardDavStore, path, properties); + this.properties = properties; + } + + @Override + protected String getComponentPathFromUid(String uid) { + return getPath().concat(uid.concat(CardDavConstants.VCARD_FILE_EXTENSION)); + } + + @Override + protected DavPropertyNameSet getPropertyNamesForFetch() { + DavPropertyNameSet addressbookProps = super.getPropertyNamesForFetch(); + + addressbookProps.add(CardDavConstants.PROPERTY_NAME_ADDRESSBOOK_DESCRIPTION); + addressbookProps.add(CardDavConstants.PROPERTY_NAME_SUPPORTED_ADDRESS_DATA); + addressbookProps.add(CardDavConstants.PROPERTY_NAME_MAX_RESOURCE_SIZE); + + addressbookProps.add(DavPropertyName.DISPLAYNAME); + addressbookProps.add(SecurityConstants.OWNER); + + return addressbookProps; + } + + @Override + protected ReportType getQueryReportType() { + return ReportType.register(CardDavConstants.PROPERTY_ADDRESSBOOK_QUERY, + CardDavConstants.CARDDAV_NAMESPACE, + PrincipalMatchReport.class); + } + + @Override + protected ReportType getMultiGetReportType() { + return ReportType.register(CardDavConstants.PROPERTY_ADDRESSBOOK_MULTIGET, + CardDavConstants.CARDDAV_NAMESPACE, + PrincipalMatchReport.class); + } + + @Override + protected DavPropertyNameSet getPropertyNamesForReports() { + DavPropertyNameSet addressbookProperties = new DavPropertyNameSet(); + addressbookProperties.add(CardDavConstants.PROPERTY_NAME_ADDRESS_DATA); + addressbookProperties.add(DavPropertyName.GETETAG); + return addressbookProperties; + } + + @Override + public Optional getDescription() throws PropertyParseException { + return getProperty(CardDavConstants.PROPERTY_NAME_ADDRESSBOOK_DESCRIPTION, String.class); + } + + @Override + public void setDescription(String description) throws DavException, IOException { + DavPropertySet updateProperties = new DavPropertySet(); + updateProperties.add(new DefaultDavProperty(CardDavConstants.PROPERTY_NAME_ADDRESSBOOK_DESCRIPTION, description)); + + patchProperties(updateProperties, new DavPropertyNameSet()); + } + + @Override + public Optional getMaxResourceSize() throws PropertyParseException { + return getProperty(CardDavConstants.PROPERTY_NAME_MAX_RESOURCE_SIZE, Long.class); + } + + @Override + protected List> getComponentsFromMultiStatus(MultiStatusResponse[] msResponses) { + List> vCards = new LinkedList>(); + + for (MultiStatusResponse response : msResponses) { + VCard vCard = null; + String eTag = null; + DavPropertySet propertySet = response.getProperties(DavServletResponse.SC_OK); + + if (propertySet.get(CardDavConstants.PROPERTY_NAME_ADDRESS_DATA) != null) { + String addressData = (String) propertySet.get(CardDavConstants.PROPERTY_NAME_ADDRESS_DATA).getValue(); + vCard = Ezvcard.parse(addressData).first(); + } + + if (propertySet.get(DavPropertyName.GETETAG) != null) + eTag = (String) propertySet.get(DavPropertyName.GETETAG).getValue(); + + if (vCard != null) + vCards.add(new ComponentETagPair(vCard, Optional.fromNullable(eTag))); + } + + return vCards; + } + + @Override + protected void putComponentToServer(VCard vCard, Optional ifMatchETag) + throws IOException, DavException, InvalidComponentException + { + if (vCard.getUid() == null || vCard.getUid().getValue() == null) + throw new InvalidComponentException("Cannot put a VCard to server without UID!", false, + CardDavConstants.CARDDAV_NAMESPACE, getPath()); + + vCard.getProperties().add(new ProductId(((CardDavStore)getStore()).getProductId())); + + String vCardUid = vCard.getUid().getValue(); + PutMethod putMethod = new PutMethod(getComponentPathFromUid(vCardUid)); + + if (ifMatchETag.isPresent()) + putMethod.addRequestHeader("If-Match", ifMatchETag.get()); // TODO: constant for this. + else + putMethod.addRequestHeader("If-None-Match", "*"); // TODO: constant for this. + + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + Ezvcard.write(vCard).go(byteStream); + + putMethod.setRequestEntity(new ByteArrayRequestEntity(byteStream.toByteArray(), + CardDavConstants.HEADER_CONTENT_TYPE_VCARD)); + + + byteStream = new ByteArrayOutputStream(); + Ezvcard.write(vCard).go(byteStream); + + try { + + getStore().getClient().execute(putMethod); + int status = putMethod.getStatusCode(); + + if (status == DavServletResponse.SC_REQUEST_ENTITY_TOO_LARGE || + status == DavServletResponse.SC_FORBIDDEN) + { + throw new InvalidComponentException("Put method returned bad status " + status, false, + CardDavConstants.CARDDAV_NAMESPACE, getPath()); + } + + if (putMethod.getStatusCode() < DavServletResponse.SC_OK || + putMethod.getStatusCode() > DavServletResponse.SC_NO_CONTENT) + { + throw new DavException(putMethod.getStatusCode(), putMethod.getStatusText()); + } + + } finally { + putMethod.releaseConnection(); + } + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/webdav/carddav/CardDavConstants.java b/flock/src/main/java/org/anhonesteffort/flock/webdav/carddav/CardDavConstants.java new file mode 100644 index 0000000..3bdf10e --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/webdav/carddav/CardDavConstants.java @@ -0,0 +1,68 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ +package org.anhonesteffort.flock.webdav.carddav; + +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.xml.Namespace; + +/** + * Programmer: rhodey + */ +public class CardDavConstants { + + public static final Namespace CARDDAV_NAMESPACE = Namespace.getNamespace("C", "urn:ietf:params:xml:ns:carddav"); + + public static final String RESOURCE_TYPE_ADDRESSBOOK = "addressbook"; + + public static final String HEADER_CONTENT_TYPE_VCARD = "text/vcard"; + public static final String VCARD_FILE_EXTENSION = ".vcf"; + + public static final String PROPERTY_ADDRESSBOOK_HOME_SET = "addressbook-home-set"; + public static final String PROPERTY_ADDRESSBOOK_DESCRIPTION = "addressbook-description"; + public static final String PROPERTY_MAX_RESOURCE_SIZE = "max-resource-size"; + public static final String PROPERTY_SUPPORTED_ADDRESS_DATA = "supported-address-data"; + public static final String PROPERTY_ADDRESS_DATA = "address-data"; + public static final String PROPERTY_ADDRESSBOOK_QUERY = "addressbook-query"; + public static final String PROPERTY_ADDRESSBOOK_MULTIGET = "addressbook-multiget"; + + public static final DavPropertyName PROPERTY_NAME_ADDRESSBOOK_HOME_SET = DavPropertyName.create( + PROPERTY_ADDRESSBOOK_HOME_SET, + CARDDAV_NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_ADDRESSBOOK_DESCRIPTION = DavPropertyName.create( + PROPERTY_ADDRESSBOOK_DESCRIPTION, + CARDDAV_NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_MAX_RESOURCE_SIZE = DavPropertyName.create( + PROPERTY_MAX_RESOURCE_SIZE, + CARDDAV_NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_SUPPORTED_ADDRESS_DATA = DavPropertyName.create( + PROPERTY_SUPPORTED_ADDRESS_DATA, + CARDDAV_NAMESPACE + ); + + public static final DavPropertyName PROPERTY_NAME_ADDRESS_DATA = DavPropertyName.create( + PROPERTY_ADDRESS_DATA, + CARDDAV_NAMESPACE + ); +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/webdav/carddav/CardDavStore.java b/flock/src/main/java/org/anhonesteffort/flock/webdav/carddav/CardDavStore.java new file mode 100644 index 0000000..d7e9db3 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/webdav/carddav/CardDavStore.java @@ -0,0 +1,349 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.webdav.carddav; + +import com.google.common.base.Optional; + +import org.anhonesteffort.flock.webdav.WebDavConstants; + +import org.anhonesteffort.flock.webdav.AbstractDavComponentStore; +import org.anhonesteffort.flock.webdav.DavClient; +import org.anhonesteffort.flock.webdav.DavComponentStore; +import org.anhonesteffort.flock.webdav.ExtendedMkCol; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.commons.httpclient.Header; +import org.apache.jackrabbit.webdav.DavConstants; +import org.apache.jackrabbit.webdav.DavException; +import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.MultiStatus; +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.Status; +import org.apache.jackrabbit.webdav.client.methods.DeleteMethod; +import org.apache.jackrabbit.webdav.client.methods.MkColMethod; +import org.apache.jackrabbit.webdav.client.methods.PropFindMethod; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; +import org.apache.jackrabbit.webdav.property.DavPropertySet; +import org.apache.jackrabbit.webdav.property.DefaultDavProperty; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +/** + * Programmer: rhodey + */ +public class CardDavStore extends AbstractDavComponentStore + implements DavComponentStore +{ + protected Optional addressBookHomeSet = Optional.absent(); + + public CardDavStore(String hostHREF, + String username, + String password, + Optional currentUserPrincipal, + Optional addressBookHomeSet) + throws DavException, IOException + { + super(hostHREF, username, password, currentUserPrincipal); + this.addressBookHomeSet = addressBookHomeSet; + } + + public CardDavStore(DavClient client, + Optional currentUserPrincipal, + Optional addressBookHomeSet) + { + super(client, currentUserPrincipal); + this.addressBookHomeSet = addressBookHomeSet; + } + + @Override + public Optional getCurrentUserPrincipal() throws DavException, IOException { + if (currentUserPrincipal.isPresent()) + return currentUserPrincipal; + + DavPropertyNameSet props = new DavPropertyNameSet(); + props.add(WebDavConstants.PROPERTY_NAME_CURRENT_USER_PRINCIPAL); + + String propFindUri = getHostHREF().concat("/.well-known/carddav"); + PropFindMethod propFindMethod = new PropFindMethod(propFindUri, + props, + PropFindMethod.DEPTH_0); + + try { + + getClient().execute(propFindMethod); + propFindMethod.getResponseBodyAsMultiStatus(); + + } catch (DavException e) { + + if (e.getErrorCode() == DavServletResponse.SC_MOVED_PERMANENTLY) { + Header locationHeader = propFindMethod.getResponseHeader("location"); // TODO: find constant for this... + if (locationHeader.getValue() != null) { + currentUserPrincipal = super.getCurrentUserPrincipal(locationHeader.getValue()); + return currentUserPrincipal; + } + } + else + throw e; + + } finally { + propFindMethod.releaseConnection(); + } + + return Optional.absent(); + } + + public Optional getAddressbookHomeSet() + throws PropertyParseException, DavException, IOException + { + if (addressBookHomeSet.isPresent()) + return addressBookHomeSet; + + if (!getCurrentUserPrincipal().isPresent()) + throw new PropertyParseException("DAV:current-user-principal unavailable, server must support rfc5785.", + getHostHREF(), WebDavConstants.PROPERTY_NAME_CURRENT_USER_PRINCIPAL); + + DavPropertyNameSet principalsProps = new DavPropertyNameSet(); + principalsProps.add(CardDavConstants.PROPERTY_NAME_ADDRESSBOOK_HOME_SET); + principalsProps.add(DavPropertyName.DISPLAYNAME); + + String propFindUri = getHostHREF().concat(getCurrentUserPrincipal().get()); + PropFindMethod propFindMethod = new PropFindMethod(propFindUri, + principalsProps, + PropFindMethod.DEPTH_0); + + try { + + getClient().execute(propFindMethod); + + MultiStatus multiStatus = propFindMethod.getResponseBodyAsMultiStatus(); + MultiStatusResponse[] msResponses = multiStatus.getResponses(); + + for (MultiStatusResponse msResponse : msResponses) { + DavPropertySet foundProperties = msResponse.getProperties(DavServletResponse.SC_OK); + DavProperty homeSetProperty = foundProperties.get(CardDavConstants.PROPERTY_NAME_ADDRESSBOOK_HOME_SET); + + for (Status status : msResponse.getStatus()) { + if (status.getStatusCode() == DavServletResponse.SC_OK) { + + if (homeSetProperty != null && homeSetProperty.getValue() instanceof ArrayList) { + for (Object child : (ArrayList) homeSetProperty.getValue()) { + if (child instanceof Element) { + String addressbookHomeSetUri = ((Element) child).getTextContent(); + if (!(addressbookHomeSetUri.endsWith("/"))) + addressbookHomeSetUri = addressbookHomeSetUri.concat("/"); + + addressBookHomeSet = Optional.of(addressbookHomeSetUri); + return addressBookHomeSet; + } + } + } + + // OwnCloud :( + else if (homeSetProperty != null && homeSetProperty.getValue() instanceof Element) { + String addressbookHomeSetUri = ((Element) homeSetProperty.getValue()).getTextContent(); + if (!(addressbookHomeSetUri.endsWith("/"))) + addressbookHomeSetUri = addressbookHomeSetUri.concat("/"); + + addressBookHomeSet = Optional.of(addressbookHomeSetUri); + return addressBookHomeSet; + } + } + } + } + + } finally { + propFindMethod.releaseConnection(); + } + + return Optional.absent(); + } + + @Override + public void addCollection(String path) + throws DavException, IOException + { + addCollection(path, new DavPropertySet()); + } + + public void addCollection(String path, DavPropertySet properties) + throws DavException, IOException + { + ArrayList resourceTypes = new ArrayList(); + resourceTypes.add(DavPropertyName.create(DavConstants.XML_COLLECTION, DavConstants.NAMESPACE)); + resourceTypes.add(DavPropertyName.create("addressbook", CardDavConstants.CARDDAV_NAMESPACE)); // TODO: constant for this... + properties.add(new DefaultDavProperty>(DavPropertyName.RESOURCETYPE, resourceTypes)); + + CardDavCollection collection = new CardDavCollection(this, getHostHREF().concat(path), properties); + MkColMethod mkColMethod = new MkColMethod(collection.getPath()); + ExtendedMkCol extendedMkCol = new ExtendedMkCol(properties); + + try { + + mkColMethod.setRequestBody(extendedMkCol); + getClient().execute(mkColMethod); + + if (!mkColMethod.succeeded()) + throw new DavException(mkColMethod.getStatusCode(), mkColMethod.getStatusText()); + + } finally { + mkColMethod.releaseConnection(); + } + } + + public void addCollection(String path, + String displayName, + String description) + throws DavException, IOException + { + DavPropertySet properties = new DavPropertySet(); + properties.add(new DefaultDavProperty(DavPropertyName.DISPLAYNAME, displayName)); + properties.add(new DefaultDavProperty(CardDavConstants.PROPERTY_NAME_ADDRESSBOOK_DESCRIPTION, description)); + addCollection(path, properties); + } + + // TODO: I don't like this... + public static List getCollectionsFromMultiStatusResponses(CardDavStore store, + MultiStatusResponse[] msResponses) + { + List collections = new LinkedList(); + + for (MultiStatusResponse msResponse : msResponses) { + DavPropertySet foundProperties = msResponse.getProperties(DavServletResponse.SC_OK); + String collectionUri = msResponse.getHref(); + + for (Status status : msResponse.getStatus()) { + if (status.getStatusCode() == DavServletResponse.SC_OK) { + + boolean isAddressbookCollection = false; + DavPropertySet collectionProperties = new DavPropertySet(); + + DavProperty resourceTypeProperty = foundProperties.get(DavPropertyName.RESOURCETYPE); + if (resourceTypeProperty != null) { + + Object resourceTypeValue = resourceTypeProperty.getValue(); + if (resourceTypeValue instanceof ArrayList) { + for (Node child : (ArrayList) resourceTypeValue) { + if (child instanceof Element) { + String localName = child.getLocalName(); + if (localName != null) + isAddressbookCollection = localName.equals(CardDavConstants.RESOURCE_TYPE_ADDRESSBOOK); + } + } + } + } + + if (isAddressbookCollection) { + for (DavProperty property : foundProperties) { + if (property != null) + collectionProperties.add(property); + } + collections.add(new CardDavCollection(store, collectionUri, collectionProperties)); + } + + } + } + } + + return collections; + } + + @Override + public Optional getCollection(String path) throws DavException, IOException { + CardDavCollection targetCollection = new CardDavCollection(this, path); + DavPropertyNameSet collectionProps = targetCollection.getPropertyNamesForFetch(); + PropFindMethod propFindMethod = new PropFindMethod(path, collectionProps, PropFindMethod.DEPTH_0); + + try { + + getClient().execute(propFindMethod); + + MultiStatus multiStatus = propFindMethod.getResponseBodyAsMultiStatus(); + MultiStatusResponse[] responses = multiStatus.getResponses(); + List returnedCollections = getCollectionsFromMultiStatusResponses(this, responses); + + if (returnedCollections.size() == 0) + Optional.absent(); + + return Optional.of(returnedCollections.get(0)); + + } catch (DavException e) { + + if (e.getErrorCode() == DavServletResponse.SC_NOT_FOUND) + return Optional.absent(); + + throw e; + + } finally { + propFindMethod.releaseConnection(); + } + } + + @Override + public List getCollections() + throws PropertyParseException, DavException, IOException + { + Optional addressbookHomeSetUri = getAddressbookHomeSet(); + if (!addressbookHomeSetUri.isPresent()) + throw new PropertyParseException("No addressbook-home-set property found for user.", + getHostHREF(), CardDavConstants.PROPERTY_NAME_ADDRESSBOOK_HOME_SET); + + CardDavCollection hack = new CardDavCollection(this, ""); + DavPropertyNameSet addressbookProps = hack.getPropertyNamesForFetch(); + + PropFindMethod method = new PropFindMethod(getHostHREF().concat(addressbookHomeSetUri.get()), + addressbookProps, + PropFindMethod.DEPTH_1); + + try { + + getClient().execute(method); + + MultiStatus multiStatus = method.getResponseBodyAsMultiStatus(); + MultiStatusResponse[] responses = multiStatus.getResponses(); + + return getCollectionsFromMultiStatusResponses(this, responses); + + } finally { + method.releaseConnection(); + } + } + + @Override + public void removeCollection(String path) throws DavException, IOException { + DeleteMethod deleteMethod = new DeleteMethod(getHostHREF().concat(path)); + + try { + + getClient().execute(deleteMethod); + + if (!deleteMethod.succeeded()) + throw new DavException(deleteMethod.getStatusCode(), deleteMethod.getStatusText()); + + } finally { + deleteMethod.releaseConnection(); + } + } +} diff --git a/flock/src/main/java/org/anhonesteffort/flock/webdav/carddav/DavContactCollection.java b/flock/src/main/java/org/anhonesteffort/flock/webdav/carddav/DavContactCollection.java new file mode 100644 index 0000000..9f4ca69 --- /dev/null +++ b/flock/src/main/java/org/anhonesteffort/flock/webdav/carddav/DavContactCollection.java @@ -0,0 +1,42 @@ +/* + * * + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * / + */ + +package org.anhonesteffort.flock.webdav.carddav; + +import com.google.common.base.Optional; +import ezvcard.VCard; + +import org.anhonesteffort.flock.webdav.DavComponentCollection; +import org.anhonesteffort.flock.webdav.PropertyParseException; +import org.apache.jackrabbit.webdav.DavException; + +import java.io.IOException; + +/** + * Programmer: rhodey + */ +public interface DavContactCollection extends DavComponentCollection { + + public Optional getDescription() throws PropertyParseException; + + public void setDescription(String description) throws IOException, DavException; + + public Optional getMaxResourceSize() throws PropertyParseException; + +} diff --git a/flock/src/main/res/drawable-hdpi/alert_error_dark.png b/flock/src/main/res/drawable-hdpi/alert_error_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..4ae44491ee6d84319192b2dc299a2e327ab24392 GIT binary patch literal 1416 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 n^W^zu`z%`IzHgoi(2{R{xf+Eh)OD%bumGAd+p literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/content_copy.png b/flock/src/main/res/drawable-hdpi/content_copy.png new file mode 100644 index 0000000000000000000000000000000000000000..623b7150486eb0c09c4dd866e672aa94337ce8f9 GIT binary patch literal 1373 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(a_b<&Bf8w$-u=B$h0&uv~)Fba&~kx zH8eMIGKA@M$xklLP0cHT=}kfCb;79^lo)agfHu3N7G;*DrnnX5=PH0bZIy}JEfzS< zgX&Ge?G{U%di8;h(Fa8>QdGl)fawRsgeP1e2cGm(^ML8S2$-;$RILs%Ffg9?ba4!+ zxb-F~*6WCYfZwWj{5!OiAx%f8IgnHIH~}KKm}*SHX~a&^=J(ZtGRE za}PugU&?!}7Hn?CEPuEC!jscA#*=-*rISNWh^;)}%fZ3u9=%3N;P^*1n{6Fy-gjJY z;3;615aacjbflL-w@&=RqLVe+2|N;01Pi_~_+Hw?p;Hl6ljRw)*|Q^MvS83H&Bu;b zxxeOaJl2r9<8yw4^MTdP>IK5n7~LC0Z%tUi$+WZAW4qorCJWWbea!P_sZ6*nl#uN9 zcM*%QcbDyftb;S2uX<3X*wB4H`J7+v0abCvyAEYt7OG36PWZLWW^gaNwfxNyxg#<+ zGgX!cDjMV;e<{hseTk2C^UN8q{~&jPu9-uYM48>YM#x^sP|Xru$*xz`&4^G@_OZZZbloUFQV^{@4tPj l@l!anq5{jIZT^gG3|ppF2!DIDbn2{ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/content_discard.png b/flock/src/main/res/drawable-hdpi/content_discard.png new file mode 100644 index 0000000000000000000000000000000000000000..e9ce89e0460b368de996f6b1d7ee149ae5fb1315 GIT binary patch literal 1624 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$=JZnz{SMW$-u?X(ACh=#L&{!#L3yw z&D7A`#K{n**Cju>G&eP`1g19ytk=TD+111qr(RHE$SnZc?2=lPS(cjOR+OKs0QR(1 zCT_PF;WQ7bHwCL(!2U4CsaGH97=2LGB1JV!2$+6AOnAZta^OinH4m8Hi+~B+%vbIe z0|V13PZ!6Kid%1H?#{d7Akt=gFXxa}LsLURu0o7zi^jA`<_i`+W?Zm*$s@~*%NH+R zXvlc6{oy5tLrPp7J3JkH%r++T{}ohGWQnO{QGEW{Jdi7HLD-f z+p}1=Fhw!`pV?5iKfX+p<@rIo34TA$YW)aEOEHiL`ILU_VbUB%^8@Q9y`J^Gn(bXv zuDCg`ESCj0pLF-4;}-W>pZ|E7vZIDc(%!zfHJLN8PF=B-GiJU(Trc;9k_zTMvc)a5QEt0Y)$g(v$yFCgT?QxCiS=hvNQAb?{wZXE^IeT%-r3ZrgJKc zyVK&hg_vM4!<| z7sK0JLlV^|7eqgB)M$NBJD({@ZSk5rnd_3m76!a7sPl4~Et?_5c)#NCrsf=V?u2uJ zTW)3;itq67iswChBGuC;T6sQ`nT)xbX0F+-ATy&J)-}eBu~(eCwq2{`@{m) zD~{#lSYCLj%tQV*@4p)6@Y0fu&^BP&THD}u+@Fz+;rN|9?#>?y0zqZHr>mdKI;Vst0H|?TumAu6 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/content_edit.png b/flock/src/main/res/drawable-hdpi/content_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..4a939679e467f958425dc509aa772530a201e13c GIT binary patch literal 1880 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 U|3Gijvj6}907*qoM6N<$f);=jbpQYW literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_btn_check_off_disabled_focused_holo_light.png b/flock/src/main/res/drawable-hdpi/flocktheme_btn_check_off_disabled_focused_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..8f0ac0c306b083ab81d1ce417427923edd8a3431 GIT binary patch literal 395 zcmV;60d)R}P)ju!0-l2z zuU!Xr2B`1+J#Ynl0+sW4hlRie@CH0~?S$l!h=7>g@K1|XTz>zah0bH{F&b>DNm4|> zw7zBp?g1LrVilJU%Vg{@qeMPQ3h^!*y_Bsxp95=?wE{z6Y`VL)UzK&1Wj~f{_e#^$ zYF#hS@-b-u-!y=48o)OV;F|{UO#}F*0esT{zG(p8Z2yUew*Rn)&S|sAD8Oj2Y3h#N zs+HC?L`(vNb?KBJq1Y0iUlO1M3U<}v)TDH>)KrV_bhL6HFL$qjTj0`ZG`SDp552xhB!z?N*%I^bl^V40A|EmezKDUD;X#Xuur)Ju7RUIRXO+s9w!OU zZ39~a)H{C*oC6;~*}J^I1;7dL0^GHogyc#X;FPNP`y`9Q;wu>CDuvV1rt?h!EbDI? z=pUd_CRrRly&m647Vf|RHp${uI^T#-*=Z;1oj1S(uyd=gRRk z1)TLNP3{d?57OLi)J@)i@&8;UP)ghDN-3od+czdbSDY%7EnENq002ovPDHLkV1j2j BvhV-^ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_btn_check_off_holo_light.png b/flock/src/main/res/drawable-hdpi/flocktheme_btn_check_off_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..b1b52b23ab9202afe71be968c2c86d69cb2e3f8e GIT binary patch literal 242 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUtot`d^AsLNtZ*1f|Y#`A3(B4g1 zLU++l8>dx06L#kHiX2nsn5^U_%<-b-sOfv=@_m|mO<|Q=yUr@RI&I|v*&DX5_nd`3e37}N$vnG!*~HZWjd#z)F>kG(8nkb3 z>@@B6js*-CMHFT+xdb<4a0&P_F4A-`VeMGPaMA7F5zZ5Dd8?P6Ibd_I>DIp9%52f_ o`22?E^Y_wTC1nTYgRGCwFX)n)7-_vq1n3C{Pgg&ebxsLQ0D$3Kh5!Hn literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_btn_check_off_pressed_holo_light.png b/flock/src/main/res/drawable-hdpi/flocktheme_btn_check_off_pressed_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..e5f0ddf5012044700f9214ddd4d5d43110165f20 GIT binary patch literal 423 zcmV;Y0a*TtP)fj*}~d5{#2Z0t=}NyW7Uo#+|6>tv%>_ z){pTZ0)WkCGXt;yFayB2Z6W{<;IQ3p&(w#105H|fCB_4IXLN5qx9~uhdBMmt{1-C@ zV6;>G0>1SPdyyibO%c#0djZiA8e?SV+)nRavWQ3HGNhwXOwQt6l!GjDTgs^nZJxOO*ycCEiOeT~4d_LbxYro@k zItA~&cviFD#6>`xBA`tX(547zQv|dr0@@S-ZHj<4ML-+zKXFRQv~9aO9*$?^`RwizE+QYij^6V>h-WHKHKQ9IL9TWwFL3`%PQ~v?&7G z{0o6=e~|u?$ms4I$p9eTTo;J|KG0vc`2>p3an892 RnQH(5002ovPDHLkV1ftlwow28 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_btn_check_on_disabled_focused_holo_light.png b/flock/src/main/res/drawable-hdpi/flocktheme_btn_check_on_disabled_focused_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..99c2a01b7fc64b09681fe8e5542efe3ddb629a6a GIT binary patch literal 793 zcmV+!1LpjRP)LvBLR@8QXV2XpgUKaV2POT|I)AYK(uQcGG( zDgFtijqBFK%(T;VGdugQLEIlKNz>$==XvMdotYPEYHDg~{%0C#UpQJN1e(A!(9$0V z1;z}9UgJiWFL{H7u74h_5}?E{0(XJSc?Z(i;>{HS5Ap_d8qiz-?g3YTJ`m>|D@pvB z@r4i}fRr-w37TfW%)mvI_^0bz^U<&0VUZGIGKi0Vzclw_0EYkwAzHv~AQnQrmr@?2 z#qybf<62$cnvb4*xOqK?W)~YE_q6zBlmUQH2=Ok>L>2>Bng0Z&X2+9(7T-pRpVFVV z0fZ25rIdqFKlU+@RPzT?%KfZLld#0MIS~)4bg!YFd?O7Ms^y|M#Qy;{fmc$>pRO2V zGvFb9O-i|$7H2F5tfx&O@h}DhLWm|x{4xc_gWpYTaGKRwy$bY3J^HkGXW6`7k=v}W zgt~Ee8eR5si}J;KhTjL;`tKz1rfg=C#OusY0}p{msDwBvY+z^sx}*i3GBH#(iQ<8t z{+`k0RUL&42q6O0M$cY=rqGL$5TcnPeuI4O4VBD5YA}b1EaEd!`bwN&BCmL_lV2rUi6Ik}iH8dJLfXW0;6>SGW0Pes zlo=`IPL=Ut%m_~#8|I@DXVgb-zGIkSWHA=9CLb|NzGZ{!)w2WyN|V~-+>fuOUn z8*=O8=c0|RQOr(6ac5ySBtvPZ8HiCk!|$lp;^WT3Zr;|SF9E8Zxs6Bf$!7-oz%$Be z?)vf-cnPHTp7ONUO=Gs~nJ_V9ROa`AL8I$F6Qmk|<2Jh)3es7~Q00000NkvXXu0mjfEY@?K literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_btn_check_on_disabled_holo_light.png b/flock/src/main/res/drawable-hdpi/flocktheme_btn_check_on_disabled_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..cd02122bea74a53223ec5c31f35faf9ffa77c6da GIT binary patch literal 624 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}trX+877l!}s{b%+Ad7K3vk;OpT z1B~5HX4^3^F!p-7IEF+VemiZW-(dq0SAQ=r8L>2#I)+lFmA+CR_cg9h)#RSk9_Ezf zd}hKnrqX1Gm0DMMmT2($<_2$k+Sgg!_cty_^46Bj%g@w>ymq{CYjX-H(lzG8(%k1by&^#BePQL zXiCiWkH0yu$z>egvgpO7uLrIFx|+6%-z!{vRYb1h`P`Vmiifs+T|pB+KWZ+w(K6$j zS=62QuRu1#(#d_^h}6Vqj2epWv87tC-go%^zzmxTgk(>{5L$gxVq-` z+ubg+&2n#5Jmt2Su~u?^o|q9ZhEz*jBT7;dOH!?pi&B9UgOP!up{}8Uu8~EEp`n$r zxs{2Du7SCgfx&vy-RvkDa`RI%(<;$5m{}PcK{Q0{4_OA(APKS|I6tkVJh3R1!7(L2 gDOJHUH!(dmC^a#qvhZZ84Nwt-r>mdKI;Vst0It3D=|w^#Hel4OGiB@;8)i zkS8?}AOfhM+Q>bcIH}2Rk*va`D*(ULBa1A8(iP*dm{tG za(^N?OoW;hAbu$V7|*?s^CvsVr2Qoh94GQxp5tfmA~Zb5&(MgxR>Sf0jGp61mpXp9 z+6`0f%+h@a($D7M;RtqjZvlVn#QxlL3?b-3VS6tg0m^lAbMqYfMvPcaM;8(dbO>19 zK(1SR@zl?n&AX16YYH$n0020WemcLRdn@18bbQzDTnr;TYN_4>tfj>Wgri-vu0xa8 z1JJkk!FUFnD~>L_numLR!4U)xw#})V|8`B*%e!;`x`gm31+@aAfhEZbp{=h&(B~EF z+xlQSEpGa-GP^bp?g7xYvJR$pyj!is~5)tb^cQb( zM(fPAWtG`=`55a_CJ-gSb|=-fvoZY}`?}7n2t=TeM20vwaU{@5C{kjI=KDGT`ue|N zusyzV(}5RTS7S8jHi8H^M^ar|Yv-MCmUOnse;|Z>>!8WCqbW-vU)=oKHCccl*B&q{z@7)D zk_YhCb9cpkEuQxFUjL#6sZypvCpSE}jf3->R)2bPwXe?&UDYe?1$3wsM4)px#kHd; z(_$;K5zBi7Kio2LvI?XYfXFAaLxpoJVEk40k}S5>e_b2Y_S(RLdXN~PWU-ubt1qQi+sES0)t2)0*DB&t;pV1T5q;= zj-mL_LH=NjLITXF z5wi$sh!G9^5kbRX8{3Mb>(<*9^v|4^W$P9;#^}Zc(?ni~nwhr11m^Txs}6NN|G}Xq2{RGDN^F?v zUxC8H!hR(1#1!C*&T-&4!W(&xpTQFtc#fZ8AiUAQ@pH2R$Br!X{CK$sminhE_wLU- zQ;d5f*xj=Q{PpAeit;doSO~>AJe?5Y9S+BA`oRE&veP`Y`iX!{%Lw zt#zds8vpv8Ml8LuA6ay{ z7`J_~5yAm%o82(~?Ye?jb{G9~0SUq;5RvO;L@3(>hulmNl)gXL$k>EoJ(^~LZH zfR0M%U{3dkjagrJZMoKp_<9gW;=T~^JeMpL3ebiKO;%|YGoA0^EN8j(wwi?vB^c`k zzoG9r4whK<0qCgyqE>NtC0KF4pUs#$ntB}OH6U?Kz8 zZs&AYHq1NWt?;x02q0FxG({{Cp_E`YF70svfFSl`PK9@UEr3}7vH)0^Hp|%y^T~;K zchI5EYj8n>j|;k;{-5{i1(EhpvgaAA>%Xx1OP^{AG1yyY@eT8{`-pR zzW4XF_2C(oe7g^(pF$h9f3vu$1mm#{OJu+~Xpy!z=iA+7q6@$nfMC>U5eW}-dIiLt zAl*L9I&H9halm?}1QP)qD)?>XFOwUq|Llp8@95{u_PgtB-!5q`#hqBp6FK5}J>-)S z(hD8{U)T4n>8S6GPY%3zx>&wD7|A5T5q!0vuH*v0W}`U3nY}l7(+{I-HXdExT8cXZ z8uD#}7HLOIfl9vI@%j}+Vn3+}_I==i4s&n4aEt714YarSO)g%LBj=li=!3gA@Y(#9 z)n9h44*C2LYF}+H#aOS}FHC^W;cRI~OTOJ*=2ko(2v5SFfV_H?PV{D$t^jpIiRGU2 z#b=iU559K&qeZ#?d4e)xU=L85Dt3C1NuXV zt%zX&q55|`PFv%=+P_D0r7%6syk^<2 zB5MRFlLXymqN}peYVRD$m53bQKNJQ$x3Zk<$Jy#h*p zRy;P=vDWR}cFAgY-PDR8GVs^!?Cz>2HQV@E@&H0Ik?C<$l21nfErb;j1mN#j>u%lA zY_)rCJuQX8k(l=kTck}F^Xy`|r!`7`oFxw+JRutr=K86iYZG&OKne?00rcx0jc~_W zchj;{OM~C9fQ?KN*c#^6dMmEd6_(93O3VWZQsqMv5P?h=6_&iZvE5~2Y1Ah~@Jjw4q0xHf3Gp(-fc*CDHyN>YhT(2q1WImG}qqj5STRNe5}|0m@5C#-P2a zgvU5zi7Kq_n13h&mtff32=s`LH)IQOP*_yy+CN;}YruJdT>*k~T%Up`j zDtzi8WhCzl;&3>g)5B-lJ}t6eE*>OIGRPo<3^K?dgAAq#{{iJ%-G&6~6`cS8002ov JPDHLkV1nWM)E)o; literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_btn_check_on_pressed_holo_light.png b/flock/src/main/res/drawable-hdpi/flocktheme_btn_check_on_pressed_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..313698a8829d46fe4c18b57cd3bb52448fd1b561 GIT binary patch literal 803 zcmV+;1Kj+HP)ll`6vzLzWB2OZa6$+%BE)b))v4+iX_;7&m>C#QKLYB=7eFl?Vd;v*!oq@3#YFiC z2vw(Rou+Z)$8ZUR&|WT=YXCg}8UV*)%K-=hoE{w= z1uTKT4WQF5S80U+esYYz{IbCVW9A;ma>A$59Dvt0MFsIiPuN1O479D4fwr|!28f7T zYc&x~%~k3MB_bY0QICjfJ9>o>=Tgdmh`2Fk%Ub(LO8M6J{jZEMS2Vs9NEAiAi;Iir z81G!+Wbm;nFU_x-~%@IVa)G6m=b07pc7`6AR5JWw%&@+S!qZ6@dUAiWOc;J0pq z_dGASyu9p{xkgkr5QgDhYwfel8O}2%Hk@jTdDK7gCRYKI4EEMx^H8i5CDuEaN(Xbc``psAJ1i5<@Q zyQbiQhPra8i71Lbt*xznYYHA{u&t<+8Y!hdH#L8%h3ZPoqNO)5O9**!9N!7Uu-5R4 zr1)hAPPw;7lTAPuc{>lv zeQy^5*{v-x1((#qCAIkanUIDPPyvUG4iDfQYHqOxsZ3Tt0kj#9qK^|LDk%;7zJnzw z`go&6FN|>c-{pgHN+T05j!P=x|86y0QVW;V!X>qENiEDsO0w0;n91bk*q*#ibJVQ8upoSx*1IXtr@Q5sC zVBqcqVMg6T|wK3?D;dFrt2j0EX!Qg@q+Jw zaKp=hm^Y>Q&K%EIvM@HxOk7*bsVLH+qNA2MLvkfc{SE2lZq+a8l5U zB_djytzKRed!M}ee*b1l(bpV>E1Nw%&1S~ff+c`zo+xA%O*o@8x$xu_*{w?-A5pnk zZEy8_)s(-U&tsnMl^2Y>e#~m`mD0QG*1dkcNaNfS(ftx_iEn<_u{S(Rs*7GWmuUw} z!6t=o5)ud48^Rgh8#AuHZ~0>5Tn(W4KlSD|i>CaGyXY=|%6-+18m0ij!qZb9oscYXDi2 z9bU`P`Aus{oe>r*9!5h3_HyeiR^+20$8tes5V6NS0J3hhevy5RSIJ)F!p(EHCOYd( zw{c10#c@d^{NHVcOIqQQR=A`UE@?$FlG<3cW0Fa-_XHB?*(8%>?{P^hT+#}cv?2vb zNkKA+B%@umO{UEy-4IPug#14wn#PGGS=_zwEW$}yvTyA5O$fZJaz7bU)YZf!^U0Vi z;1M{WoB3M;Z@?2&r>UUv#{P`JXPdaq+-*w%v~?a!O? aF^3O9sXlx4U{#a=0000EalY(fD?zZ61@MfZKjU zm97ad9x~i%v|e&T)PjXo$W(~6{Q;Nej>Ffslw>;1XUjd{JX_@3rYF&5>a)TlH$APK z_xEeW0oVF1tv2Q5fey~Mg|90bcJCpTJ9%@=rzdLh>3itGshaiJmB|X>5 zTs2BFx>?D@b0z0Y%BIgSNgaEalY(fD?zH5ZeiK->SD zp^iB@TRtf?xo_C;Rl_XIOz!BlL&_ZU4hVNF2oq~sF?*w^VrT!&Quk-8#e~ZC{YjDc z+xYoD6Wc=O0$#@CLFr>}*k-03gO zHGQ&op5>f|Z!Dql)dq^kkNUJn$ch$UQdxRIA@hEop-&Lk1rd;tT+H$==aB|8^ z&jktR-Fzo4Jq{KJD&d{9#4*TqQoX`VkuR?WR|j*Nt}DMGbN)q&-u@$(S=L1!H@l)f zOVod(LyAFs(PF`0_kMAO@s;JMcX@g_HvH#Y^j$ntUj9_O%)6W}J!XZonN7ejVeoYI Kb6Mw<&;$T^M1n#9 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_btn_radio_off_disabled_focused_holo_light.png b/flock/src/main/res/drawable-hdpi/flocktheme_btn_radio_off_disabled_focused_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..8b16cf813a2b1a5a209e168f13285233641709c7 GIT binary patch literal 1337 zcmV-91;+Y`P)@X>sGrds7R68%_ya2A$B9itcGji!#T0BOLE)Xq(#UdLP+v`=bZmJ-?`s8C(uI= zJ@jytusSWUnkE87fr!2UD*Ey*k4h)?b%a1QO%zA~W56&l2spqoUGe}Z0D0h7;0}=O z1es6-)Z5Pi)4)@Jjd;BR`1+217p?oP1l$IefVD70+9psDDzU#UTbHEtmSJT9F-zi{H ziERP%z*h&2#(@Z^@vj3H)VNh(!Scw?&8F=2LVV!*-Qzam-B=v~Utc9)8z?Nz7+;Nl z4LGg8U$H#C=n8>4<7a>!;4|Rb^t>CHdLo}%zB|~Dobw`ZUyUv6_L!3DM~;(^cs;)~ zV=BNo;0;~(xjJQEkvIr}Dc~Jd{;}n8+i#8;ROSmdA~b5zvHvAMxNPmdBTKv#G>~Kb;s-LK$OBaX+%^$i#sW zBx)PRm|``}dEhPNn0;V*WP{{Hf)?n}xrij*b>Q0NOB0S7zXPlv)OY~hv#v76T-UWN zkE_T7VF);{y2B9wP9jOR2`o&{yOGu0Knn5v9b-(nAG$Vxl;@8wERV|VKjIEhG{)?7#^zn9%px$LU)@9qrCJ@MRuPCIRizJP zG{I~nwB14huvOQZ>K;&A8X|~*gVZs#@WzlEwp%Evj;Q5PRNYQ1JA-oI7`x?fZGGq` zPYaay*i=y)+9uG^?x`CHBJh{4vGsEW330a&ticVCP}v1_*0ZW_qt&J$0$F5w-~i8S;kAX- zEb-7LD^ykz*is#*kly2GwV^3m60kgcU>WhuWHrqok{3l_R12@L#>bF)rwHV=0zRid zF9o%h`{@V12W}yC(M9cHGiY*$AyH?16e-VrWXALi(%7s8$p|E%9#}*|`7&_U7*j&J zDH|AX=Q!z}ah0*IeepQbcW(iU`)TY$HTACR+DKm;Cwx;s0lY^yntCNrKUOINSAfOo zc^BlO8={S|iVnM80^ZgA-5IOwA#p*KtpE!^ZhGFWRj&g)JlLuznSTdc#+Y(7jRU-i z9L*hd%D#<1Fo8M}^`pfsGNHSPPMo!#-&@1`nxwTc%}L-Dm2o(u#XUrZk>T!X#M1?& z4ctI(@Y-+}Br*x4JQziK-P%#Qg(U3Z40r3%f${n>@WYXe*XyB|PZIHN3dsWpcvP4C v$?hKHqf{Hs}it8_{F zBQKe~yaJcX5+7dD(SDrhdV1x711j=?)uzAB%kkQz>C3-Te^>gY=lAEtNC*3ZKF29f zbW<~Cd^`TFw72c_8_TPAe(-OJ@mdr7$m_J@4C6V>7YZI7%lT{EGhH7M4^Hm|MNxQF~?8%Gy^!Gs~y(dZ^A?%HZ3%#UerR8t3U5f$lkh5&~;`w!9VV zs}H}ep*2xSZV6xaT*WmOQ>sqLon=;8C!5`RO>@c98>MlPXBb`X%(U9PdD@1@XRgEo z`QMwD&YV|Pf5-o`jN+Aii|Ym-Hg9>z^Y!_%zBiR;zOCM{=BH5xZn>xPPZU4?>KMzra_7v6?d2JJ{++7$5M&sB>CxK9 zVbQr??O%weIS@^ujDFP;*NBpo#FA923VtQ&& XYGO)d;mK4RpdtoOS3j3^P6X_j4@oIl~*8!x@Z zh`Z333QlpRp9L7>Vj&A5tW)IIjawCUm>wNY zoq647u|E^Lff1Zpt~ZuNAS?fM91@m&1t&@swdcS2sm=eG*|_Je>Fz;%T-<@KymVT4 z=qg^rrP1Lum6iV*9?v9?mnwe0CIlLk9~Rd9E&fs-DY_o&xm~_K-Cc;I@;6)%R=t&% z_hB0z6L)0_f55rX;WUFM@Dw)SciAXQ^2Blo9K;b}sb7^UPJQ>#-tGUK+3@tcb064& zgqJa%Qks1~8AVZS5E1Gi3cP`1DWzJy>HxkYJp0R1#i_Ot$QSl`VaZ>YD*pW3H-`3{ z|JO&3;NLigD=DQVS*<9F4m^y*_%MEvQo3BP`ZB(aoA_C&qPA)R`RM#mSnMQ@9(if+ z!1d|wXK@`zQ%bj*h-gNn58ubf@bm16AK**#d$l@B9nD^542es24JXPYMOW?i#>2w$ zD^?!z+M{?Ihl`@=H@o@2FjQyL)$jKC{#dyA)(ur$p`;ELGghdu|fhdr>S(AiHiLKaQ103jg`< z2OkrmZ=xNF--U@RdniY#v*H+SZ+48Dd7x9o>{;RB9#O+wS}e}523h8qNO)Z$8O~y} zg_7hRv?9ksxw2ze)T}#9bd=y=!imAMJZ!O_= zdh1(OL0}>a*(vV9RSXtIv1n9SDYDE#QAW&Qd)Du!1&`c=MG530>RNu>H9DN&yvWS` zZPEHp^k>=Ub7DIvEPt(8LD%XIj0wx{5Gnnlu+F}mY}$e>>wq|-m-6xXWPToNwzicL zH8DNt#bS~H1!irn)>+>WMfnR zLQ3gIy=pgpVlA5bOrUYB@-B|!#qvn80sHWz$k>aGRR-KSR{0~&rj(}ZRr~Ngk#p8& zta2BLLwU2)IDzrw0u3Y@ zM~gKvp*t%^i!>Q6rj+LLm}W?X&aLj(Xz?y017f)QxHz|yBD{`?3_d^HCA_gklm|P- zG1`=gy^0s_+i7kxzGF$9M^=G#&e=v75?vk iVp_*K*0GKUi}wIPaQ6{6h4$e90000R!A0>6qze@{E`1)CLRT(=FCbD}9FZbjND(Qq zVx86?60}MQffjKw6HUZ4_uSl?g7YntJLjC=y));}KqL~0L?V&#aU^Sl*4hc60OWyO z_<0ZL0BsTZk)^-P1XydQfH|NFoCButA^_UJ4zLNl0vaOnJqy1h3$WG}fXl!QU=nx_ zyaAfPc6e+WC;=CN8N8S$z>0{pNAW+B0BdatSO8`L0iFY&MPzTl^T&W9a1F4)Tc9o? zpVG#VT7b3oJa7;A2GjwUbbp6)QU-1U$AO0;vXLf^R06`AzYlx_9*M~IkR^lcOapg- z)4+o?U*c#Arv>0HKC>*Q4IaQb*D=Pd;}8A?W6W#k+^)vPL<+w({6Vab5_}gC`GVhq zv%n?odX<3i$>)J*fYUnLhYfrZm=EbVG1@OMhc8mgSr(*V>jl1cR5WW73kaE`icftl zjgy45g6(SXl?crjC;(^hRrIfUeiVYub$r3g4|)6-8UcBHMQ!RrQ_%!Ya6kfb!Eh!$ z%m8-iZvuysCXF5{Gy=LUu`10enG8M>pD~Sq4u1QTltF1I;d`r2(ghj;ZG1mpvDPNu zqe%%iE5H^9As`f+ReZ@WY6DC_G1y+!6iMwNUc0U~ZsKdn+M&7F`jdZswHS#+B9TaBocsh1faFH$ SCHBMs00002HE+EP)>oKci;iB=ra z5wcBW9A_0)crrWvtEwJ{u8!BcGfegDZj3*vE;Cb8-|Kqyetgx0OQI+W0YC#VVQ2sf zfCvKsd>9r$2cQX~E6dVfW_{XqVo?+tz$^@Nj_@{&1~mXy09IvLcDwMHY(P;I2Ebkz zQKv5J5z&wnrAbuXBAqxPDvMg=Pq$mT5qL=~L$5U6iPIwG% zbrQ21|I>hQK=&|^*H=O>9vy%a3~sgYpvz5P1oCqL-v^LfIlb+W7%K<9bhR8{p% zbb96xhfsNdVFz{sGYjXO@!o6iy#fFsgcU+`Qp$#iE|Yne0FKXw*z+zj$By zcbs!?GP5C~FN6^HrIg1)h$R4c@AsT@2j2Tz%=`)wt)!G68Dox=Qj;<-hVc4mEIrQd zz#J0~ZPPRdZQK5mnGdCuA8V~YO_Jn92(ivh{^7m&*PGBuN&LOA`Q@=lQCtDmRAF*%b2O$^V9kj!NW%(Cnc zM0C=&?XR1rIf$JfxXu7ThRE|0fHY>NwRTq5^{)Wf*=+WK);jjdU4WFb6+%4d`~Jty zx!X#qN(d2C#1R0_^ZfK|GZKZmuJ17O%`{E#UWWV#t@R>J)4R-k*fh%N4pSS2V=}5A;jOTwQu!(ADe>|03$G=L~h&mHUN{R>Cq(<(la0; zueJUZz$~m01A#L;uoWT0zDh**lO#FWK4TiR)+a>tg>&vzW{&MSv_NF^an2b4FAE_a zY>4xhLMS)x1NcE$D+VgM5%=C3W}ZtazuuYU7Dy?NnR(B9A6pH?8xXctkLAI2fQVKA zG&4u%r0ec)uN?wI6TKB7>U7@wolEU)VCETy$~8)n0N|qy5JEIWv~bSd*qPxLIOh(C zXeoq<6}h~?pv7#3l(GTvmG}Nf-uo+eJ@38p-roZ7Rah$qR$!>Fw}Obgl=5?CzSZ~r z{?1G{pzFGQW`0FV`Pqad*$E8#WYmN-P459{+P1wjA=zvNGmEb4ehOd})`)?}R0EPE zSx6~A^4|ZVX_}im^1XnjX@2Ove?uwt*9onr1<;Hx6s-(rS$0H3kK49=uj{&vdQB|4 zuA8@Q`yLTJG{ziFnF^qjRaJ4G=L$nnw^b_@Li7M0S!;i8tvwV%{8LKV?n=I{>jJ>{ zj4^-FT0e=L>H$0{%d#H*c!|hp1OUdE<1|g*XXb;tu76wC_2HB=!OWtr>%+RPf6vS} z(lmX4Vqa`A2*5*6Tve6J^IT)7OrsR*BuSnCc;KA7WvzYNTAKn`gnY5KFZSN6uIu(! ztJPc9+INZQ#2E8ImStZ~An!5A@81B=R-y|GeP*<*VvKpLl={;stJHPPbZEUsSPXu@R>E0pK)91y;Q1S21BZ?J)BpV?-=-%&;)TCv-FQoR z7U5v8!k;S)hVT5E!UlK%J%ImUARpJKOkL%LebwMRUf7pC&STB%HVW$nV|!*?+3Pm{ Y1w2dLYuMTNQL;wH) literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_btn_radio_on_disabled_focused_holo_light.png b/flock/src/main/res/drawable-hdpi/flocktheme_btn_radio_on_disabled_focused_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..aa1e2ee02c723375bc7232310b6d289e610e4654 GIT binary patch literal 2293 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 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_btn_radio_on_focused_holo_light.png b/flock/src/main/res/drawable-hdpi/flocktheme_btn_radio_on_focused_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..d971b96929325a7cf3fd6c8d6a7819c176d0b29a GIT binary patch literal 2111 zcmV-F2*CG=P)!21&+t5v2C)R43#6Wa(6bY3gxt|ap zwp#59n=00=v>hM1O=Xi%HMw+9R_cd_SFIF6l!g#8a3M7zIEFZv8`s{4=W*lG#YrH5 zCgn(1*0t|F=lsuk&Z}XJ#%PSj=uVPjv>B#@Xn+oA>Hy_G7jPD62RcUs%uNAM)~*Kpzzo2Jw0a6is!!B+de(0V;4}~hnr{Zk z4FgCADFq$^?p2Ci1ug(BKnD;-T3rJsAU#wDQ~=YEV3WWh-~&g1^S1?nvU(lR0F)?2 z4**AjPJhTuDdn6F5mM2C6 zU;*$H(o*j@0yI9kWUlX`Wh}E(o#CDU7nZ<9GT^e z_vh6CCU6SaA%y5m2dM*og7oZP9RV6g4S)*RwMa|;%@N>VYkp8$ef;vIr+^+{N4P=6 z)$dLQ&VP0tNGc!+Am$I5g}{%2iNKpeh>moSMZgn4H}I+>K<7}pX+u4rj?Tl#!21~3 z_vDhfKBfHUb;XyqhZ}^AJR%V1#8O2ZApsw=*<9ryxRX_{tkRC_}nFIU+S%iM)51EN^j@0Te)0$pad}(`dUb7RJj0{?Ld%w5px6Ku+ z&R$M?uO(9kK)$Zoz9L6tT~+J0vb;z$GQGxu?&l6w+ef>KmLuchEtRD){*1~k>z1vI6b~CWjkD3=qJBrfIs7H6@PBSS7g>911`=vMBur=seq7PzksS z4f`Mp>&{=~vfF|EY+>qqla-h->PDUqGHuQYwd#C@fSDUqFc53bgJ^@yH|X(hh( zCl+o}09?T2Lc>0&?kgMtx|Pfc$m%_E04jjI>bA`vGP%;9R|TB)PA1-?2_!99Pla@2ygNoQmfupw=`^4LFRy^Ram!?b$GTLv=6yIwg_zN+5WZ!u7HheOVKU%&OriSkv7uc3IhAv2H@ zSqziNW2z6vXea<~WDbuiriI7~o5}OeGhMd#h4OH61j+;OLU}lOrpxwb^HRKv0H-6s zRVCYS0&`2+o-msnrZwr$U;wDYsxZ`s3DzN4?m-g7GLdcNlLHpSOgJS%_dk<~h@})@MJiGI9yZqU6*2XB%^-X)UTRN&uGPITJW8 zgh&Akkd}|!Oe@i*1l)xz=`Da2vyFR@u1x^(&9xtGET5b@yLflKp34&*eMR=--Szs+ zqA$;GuKj2u@<=9t?wD=NSNE;z_{>uGZMhU23P2}vd*BA<2q9v?UZ7s}^W92F^tHKS zLrn=S&x9+D$HgLEI&CI9`ikt1z9Rdj(`NEs`Z)@T3(wgHZU;scPn1?z+NFl z962u+BB#5YGrXQKn^oehB;E+)KG7lW+|`?>26VI*o}X@|Clw{P=6tjpIo*7d#>w- z`o9Yy&ZmP^0?%+OhI$1cd#z#vyMW#PkZAz(fd`S+A8f7S>eU1?XwM;QNsAC7o(|#$egxc)jFDX&^wjnSQ@{{r$FH15&YY`Opd002ovPDHLkV1h0|`E39I literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_btn_radio_on_holo_light.png b/flock/src/main/res/drawable-hdpi/flocktheme_btn_radio_on_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..c586ec3ee1429540e47ac623416e76577d53bca3 GIT binary patch literal 1447 zcmV;Y1z7rtP)q<&#OAJ^l(>5A{+8AF9FC<1oibOOTJ`#9pN}`Y` zXyQjA4aP(;+GzSBiNcHQVEM*EKcF8#U6z8SlztA|*>&r~+}bS$>68|D*ncwFo!y)N zIsbF-x%Zq4al{cv9C5@E$HPOybQ7woDgg@M0Bqvt2oM5vP1B;&X>ZyDsH*A&76Jvp zlYkRb1b_|<0at(qpiR@Xf$6Y&-vU%sRe)u{N+21y4731UK%W@q0y2R-U_PdpUx8Da zrs?-%|GosMs+tL`266z+X%E-etoTEJLK(4PMqd^~4ItEXYj$+&$wiKVu)PFOfp(x) z)3oakT!5;o3xKsiKTzwh&;~Fg?O=TUOJaBwGY<^R<kcMA^mTc+(dcZ0%1zb7feW2dG}J_k17_ z7_#b%ocBio%2d_S+P zW`RA}0t|?!cjNJV<5Z9P4a~gw+G;LIO zA@M#bSP>AyTt1mGMndz`<=~Ev(n(MG7aM&>x zIepXOj+#U5qmI_$$BoGnb>4OxhuTM#&5JMA2wp>Q`hYP}(a z54IF#Np0`^o+mT8sgJBXQYaT?Nf$(2qyr(rnFE}&(zRJl;tN2i&ybg4)-gITu)E-J zRc>lDu<~G;?f+(?^S0Yqd9ciuy?_%_c^@{S0l;ax4P|h7t^F`g9+hmS3>9Z z;=t}TnP>L_H!+j7cjNK=NSFTD8X@TCKp+sd;?I;iGF4SQzy{2QrgrDj78_<&s>D41 zX(rBoO{U1P6RW~8N@uBLuIpt)qD{;TI2LU(KG zujz@A$|FmZJDU0*HBIZ9st?n3tx^oUfEoR}U8{JQc&%~>IPp-gRbnB!H6If%wiNNV zj=4o%!8}VYJTezscj7@`EyfW?9C5@EM;s3m{{RWFAz#8h9-#mL002ovPDHLkV1n<% Bs2~6U literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_btn_radio_on_pressed_holo_light.png b/flock/src/main/res/drawable-hdpi/flocktheme_btn_radio_on_pressed_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..7f66e282fe4c088ce29728d8f4b4c9801c3b272c GIT binary patch literal 1908 zcmV-)2aEWLP)BS2N4M55bQt@OE4PYNQzP9_2MkndS?1p)q6g4x70g3-zD87uwkbMCA zlgY$&d8v~Dhxz%2m-S|QS>H|7LrMx{6^}3?~u!qX6)*f`d?m?~m z27s>t2u@tRqoGmlI44O`Os7*>^`n*U&{BRAmA^kJm|BO8bw60=&yCwWN#z?D0Gs3S zc%>1CT&O;x_sVCG*Yz?6yPfvWsk>%B;On6ve10iu25zh{!V{vVn(@t01C%BKk-vbu$dZTR{-a zjv;1H>$ulUJlKIc!=6UZaOs>IWLb96d;g+{L`3wVQtGzWdRHkm0{~_odGEKF`J9M6 zOGFE;_1~i?+BU}Y%Dkw;>%FG*Xz#!VdLG&=%eL}7|AB~{(OSQ2t-T%u!JblTm79FP z%55w@CD2jG_h;1r@Pk{Om*M(WHsFSAYnLN*bN<=;k!`}}E zgKcBXoQPJ&YXBf3HpVQhwYLG>^4`DTocpGT{5J@K{f?pufKHNRF`Z6*6GjJpl&@F* zCq%Rx$MMxTj_>w4oJ7QN9N&%O_)Q|(%k%ulS(a^eeZSUq007iC@;K)P^ZERWh?v1( zaMfCSXyusMIOj&p907n*Dl^8+lv0NZw4x|B7K_CdA}WT%;nf~zVg_Kx;9)b;32B;M z6p_I7 znE4l|N|2*)ikV+a(==&MT5I=+=tJ-Q1rh1mb6ACr(Z_oq0eD6!b*CNYnc37nf3~Ag zh{)NpEHAfrp)NOW1NeF^s|!r$Mx2==5!ujMf6|^W%W@DiOIZnsN^Jr;4 zBch$Yf{AEHDRqDg5|II_%C!?C0f0Ljpp?ppXy1E(AkqXuFe9Sts3?)H;5xA#NQB<| zEh3sN8}>#2qq+*#T4w-0X6CcZ{J`~;QZ5LBYeaOXtM){6CkTRT?TpOKhMCU+__&tU z1x}%=ue%6kt@TY2IaikDX8Tzj$2XNyZ=f!5kHDT%>Ww&#Z?q?iqSzFXXSLQhdK89* zLd8#ZdLazMTL7{=&o8!T>tzRdi53qVy9TDBqTE9LV4*%3%eGySbQCtGB zsAY5k11NNoBtTV(9vP9e)_GZ$VCI(r{5uGO&)P;fYc%Ke8No_G_WNsYPq;%v!rGBL8Zr#iptxqLZQ)7ig_Mu)^PKa(4?~{)NU- zC;;p%-`X5vr>E0tnIsA0%ehYK3J2hSlgVVh{9v7eGgJg>p9=GF!J+c|mCW5r1L`}e zeLQ(1T&=@e+VTJXl5gG0H+br=S=ZiDwoxDKMg8XnmEp(!OkoWeb%lva`EGqmUnx)R urv}S-YCrZ^#){8v))B_8HBRhvo6i9x_qHoRa6U@_0000%!X&lm`~=8?U0@DOMs)ZD9^3_Q zDUDnu=RgI-5wEJrlO!oA3qmu%e^52~T1h@=;cZI&#@mz+3=0{-@lYprpMsHCc7 z6oP<{qB}!F2Akyq?#{Ux65p3xC2Orj>*UEvzOw4Zj!A6tBy%3Yzc+Tas7eu%4$?2D8~&5_ d=L7dHhi_#WRjTmlBg_B*002ovPDHLkV1kkJrsDtr literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_off_disabled_holo_light.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_off_disabled_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..a7e6bc3c5d7e4f4d6129004e3bba2c67386df04d GIT binary patch literal 419 zcmeAS@N?(olHy`uVBq!ia0vp^nm}yC!3HD`7Jga>q}Y|gW!U_%O?XxI14-? ziy0WWJ3*My{N(AiKtc8rPhVH|yWCu25+(;_j{}WlH1KqB49U3n_U1vaBL)I&59%+) zPS3KvY5wBmbb&oxc0Ruq-rlbGy69e2oWL(OZI+-H>pUg=o7lJC+SSNwU3^MWB|d$U zlhC32&HJ}%7R=w##29Aiz^Zv7x%*Ydjq^!nT?F61W9MVwQP90##EBa zyj1Sy-6oMG3apwQj$EHgChE+zQC0C&19Oe?o;;e+Z0cM5`>NjQPoAe_Lw7E}x9Y6! z$NhO%dkmx3mdbsq3(B4=oxH8sIP>W4sUq9>z!o7-RtkS&kSc+ z$1sOArbwRQ9^;$3KNC3o_&(*RpIFQzb=%S8?*y~o2W!uK;5^h^@nETidIB&w89ZJ6 KT-G@yGywq43#(`V literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_off_focused_holo_light.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_off_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..3ee96e41ad7c27064f6d5d04853e72ba5336d723 GIT binary patch literal 379 zcmV->0fhdEP)A@e>iCN_>sYT=Sv zxTF>?sf9~w;gVXIk&H*H&WxE(UOW{B@)$Fnym(wv3zyWwCAE-Zmch)Qf1;Sx<3WV- zFHCgOgrt;DPJ!1Gy4;1Db(hMqPHsxr44p^XI6G3(`v8nJjaqD*d3L!1=`(Ev+yK|4 z6t=|r-IKcb0ld>88l}v<;ffUMk0~d)C58K6T?Ns=^~V1#s$xPiKz?zC;g_^O|8SQa Zz5yA!H#X!-#^wM3002ovPDHLkV1k8IrSJd% literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_off_normal_holo_light.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_off_normal_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..aba55810b26605a9bc596ab788df5d3d69d6c5a5 GIT binary patch literal 459 zcmV;+0W|)JP)bqW6u-e{BIKGO;Y*qQ=Vkb*q5(0&4vB$)?6a1Y>y znfd4sCn5kJbzMJ%VfeCZf!mrfE~=_plx6web={jOr001K5iJ0`0$6S9r$gFBQP{rk zFAezCDPrcX>y~ZXI=`0Hp>vXkVX#aAH=r1X!O~4VS>1vZq#y+;NI?qne32}fJSQYc zCIu--K?-u(Nc)(B#c_Q5*N&p-Ny~Gc`>!%d+fRx3AS@Ql)A7oagzS>$)}(eH%bpec!9LZ65)sUH$fD$N>-lxHLHB zz5vt!KDWNe1WpceZ70Z+4AQM2I{c6}`odjj_yI^+hla0?>{S2&002ovPDHLkV1hUN B!-@a^ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_off_pressed_holo_light.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_off_pressed_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..9f68344abd332993fe588644d47a5eedf92ee69e GIT binary patch literal 419 zcmV;U0bKrxP))RucD2 zBXZxQ;n01YB_o{_j)#7X)x+FhmN*8HYSR4xj+g^~L!lqpGU(Y&L7B5Ci}s zg0;4F&P6+yrZ3L7CN&Z1sRZ{B5q3ESU4qy+BFTkFav_pjh$I&x$%ROA5sc)tXE6qX zspQ$C5kNmCm`a{KBFTkFav_pj_IZl5oyxV@!mB1}fmMWLuWyi!tUlNs=pmy>FVP`P^=|&j8w? zm_x_31V9Ghnp5l-9{_3$ba^R&n}a;tu_BUOC}4O8snaiMqaWOT4qxx`c_6A_Q)mDH N002ovPDHLkV1iK}yCMJp literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_on_disabled_focused_holo_light.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_on_disabled_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..fc32c6842c4a88bd654f9a684d8d05afad7f7ba8 GIT binary patch literal 409 zcmV;K0cQS*P)>7m z0cOUna_xBWO4<#jOYk35uDxEo7kYU3yGw`Vkf0RxoWr9Gz(E00a3|H9s%pt z`7Q7+kyvU>epj-MSOKIY4b81>^ZR@~_B(G>rr>t{pF-*R@ zQ0dPGCxWWJtyZhY$z<|eQ>MMZKG->T=DmMPl4Q9HNdWNPkDPPo0A2t{rAT1=y`m@% znR#b|stRUa7DaIYV6Uz&I45@{*LIvvoH$X^iYRGCl(ZsBS`j6!h>})xM)G%_$F`s| zo&5JOI*`Y7rj!4kC}~BMv?3IaFuas+vR}g6Vj}6m zU~=P&3t=48`UE%}TU z?}st&^Un~|g0PDKC(8vs61mFn3zOi4KcL1*dmZ*rM70~b6)59hv zHoNFo;IQeUtNK$r@Ol%!Et=8^NdoF8C$)b``|$_&ki!QqBdn|sqE@j000002nF}{I>z-6}ezP3nlEjN+l3JLg z7AC2MNorw|T9~93$w-FYR(mFyNFF>H3FKpviR8g!l3JLg7TS^-@sO|DuavDVkFnVXIq1i!12cZEvk})q=W9o>Bb+@CjW4k9KHe3@jc+rWmbUz O0000(K;kC2^@&~=xky-`TFJwQ(23z#fJK$xsd!G>Cbf=p_S=hsCX zFvOZT?h}b%zKz7_G5UBjqd#v1$HTpIX*i}`5TL%b#4#jEE0UxY(MP~!GWj$bjlKXl zBN@zZE}{VbtX8X^;qajeK>#3$vMe8pqUi2iHizP3b5g6S(xwETAc?A~H0IKZg_9($NRn10Nh^}1 z6-m;HBxyx!B%A#fGoUq@eD^3?P>*R%Cf_|t(uyQ$MXZAHDuO7ys1l+A2FDQ8sC;B1 zxfO)~=msCKSI-Heun;wo^wMz(hVS{;%cr&eywQB{WjwltZO0g3>5*4Lr7Q!wfz zK=FKB54D{_bhy3uN)jUR*Sq7^3m%7fzrAzBP?LoBKANo9#gJv$$2`w_FO~l~yM%4j z>0kZeHtO`4MMDyKp7*jWJ3~zY6^LijPp8wH*=+W?-|ssSF)v4x)b)Dpi=wy&;OpXE z`b_HrINu$cVgukeYO1_!!Jk9gwlgJ3S|LFF3X<3Vq@BFs9&`8y60Ni|w#({Q00000 LNkvXXu0mjf08I3N literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_on_pressed_holo_light.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_btn_toggle_on_pressed_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..a0d649d24d9bc99ad049dcf4633d8a92b5440726 GIT binary patch literal 556 zcmV+{0@MA8P)oJ&KIg5Ddl@~d~kZbq^zCw-=atAN9$~AI?EP_E8<3+FrBnzxYM3U8& z%}f{u>oJ}d89{$5A&RPxqFZ#A1p33zp2fg1b@NmE!!Q&>q; zSV>b@NmGnQa@ez&0Y=lwcMo9%^O(_e^4()4O<^TX(Kf;8jSvDv1lM(mh)8R#tE$pk zYnT`=7?GSzCYaCXaq^hc33A*aEN$NCB9U{`c@3&^D>l(-7QN?kbzuetj6h%R4 znmTA0pms@K!Z7@qrs-$T^Cq3uZlo;BI*#KX04_~2UB@&Jzzo2hNwF9F0dPXYl$S?v ubFjbdjAA8CA%Nx`Bwl|>yZMKEo5Nqe;Mjh=;?)QM0000Nt8PBw57tXpyc7*<~=8~pH-VZ|M0#cUc%RKR+7oljIN6! zi#6SXdzOW`m3#hD`P{y~{=BzI>-n{YCo5C!R4qewR{go0ebBRLP1Kg(*1c$&Fie37 zc%^>%e7a^;RET7)w~@}7HkIqETyw8Fddhj~wLaIq^>^+um`bPv%q0IZeGwDNWevXX Q1N0Yzr>mdKI;Vst0Hoe(S^xk5 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_fastscroll_thumb_pressed_holo.png b/flock/src/main/res/drawable-hdpi/flocktheme_fastscroll_thumb_pressed_holo.png new file mode 100755 index 0000000000000000000000000000000000000000..2951911cce7d55526e9edaff557d14b7088f9049 GIT binary patch literal 463 zcmeAS@N?(olHy`uVBq!ia0vp^xlyDpy081bxnJM4jjTF5ws1`kdw5AJCaTqYb%uG!a@G(B_8mQE);Ee8Fm7AgJ=Lqc z!R~_XhhNnRd_61j54oKD_JYx0M(>Zm`ID+^t!ppIK3J%5pGmG!JNt&yeudr9>e8nV z<$l`k(v{8g{8W*^>LWjlKXBb~d$#NVyMfOY>E|2~^Ge=KWiwFF^O2BGx#oRo$(OV9 zVzT0tYD>UwD99AmTYG3}D2l^y38pLW_3Qcn=H$|6!CxntX#Vy4%WrecB;vPxSjQmDP1{zs7k>7Sd4P%noHgfq^qA_H+wRDTER)}e72gP z^7^L+{5xlsKD;EhUG#(KtB@;urC09TWBPUB>3fHFn_TtTp}2YJ@|Mj%mE>d*fo9?P a(%ycGi5l;uO%lL}V(@hJb6Mw<&;$UEN6tb3 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_ic_navigation_drawer.png b/flock/src/main/res/drawable-hdpi/flocktheme_ic_navigation_drawer.png new file mode 100755 index 0000000000000000000000000000000000000000..396e0490efe7d21fab164a38b0de3eb09acada7d GIT binary patch literal 122 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`Gjj-D=#As(H{f9@Z22tMEv{a8{W zLPA2~U+{wY6DKgu`knA1`oJLv7mFaKwmJdcT5dPvTL+>IW?gh^U|lSk`#_h0VXr#N V;Rjj^Yk`I_c)I$ztaD0e0svx~Cw2e; literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_list_activated_holo.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_list_activated_holo.9.png new file mode 100755 index 0000000000000000000000000000000000000000..ad44e63a2e3533879a12860e3b855957094a9b23 GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^+#t-s1|(OmDOUqhR-P`7Are!QQx35FpLnB(=f#}= zvOMV_3tAe{gJ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_list_focused_holo.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_list_focused_holo.9.png new file mode 100755 index 0000000000000000000000000000000000000000..6cdc96f58d3d539eeac80628ce054ac0c39c6020 GIT binary patch literal 139 zcmeAS@N?(olHy`uVBq!ia0vp^+#t-s1|(OmDOUqhL7py-Are!QQx35FpLnB(=f#}= zvOLBcGn^QEM0qlOT0|6?CyD-d7ruS!z<~{tUcx3n<)wPV4TbeA#n?PL6x!R&#h43b m^$1T`A|dAdLa`}Sm%)7!x0v;wq8C8Z89ZJ6T-G@yGywoJ`zbmA literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_list_longpressed_holo.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_list_longpressed_holo.9.png new file mode 100755 index 0000000000000000000000000000000000000000..e9196aa82520aa547f88b0d6425b59ffeb72b17e GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^+#t-s1|(OmDOUqhR-P`7Are!QQx35FpLnB(=Y>pt zeZ9QStbhZ@dZo>c1%q06Ji`9WU!YawtLAB_>=zopr0FXc*{r~^~ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_list_pressed_holo_light.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_list_pressed_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..895e51bdfbce75c9a99029f87636af33be58008c GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^+#t-s1|(OmDOUqhR-P`7Are!QQx35FpLnB(=f#|v z#_3Xt7h9MvUA}zYNHC~{$0O{|`~_M?zG|L^%8sEX9s(?cuyC{kcif|*Eb3_81S$-+V)J@yPBVWyvqG&GeYz~KYxJdw0eeyIoHJnOEfn# R{sUUY;OXk;vd$@?2>|r{LWckV literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_progress_bg_holo_light.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_progress_bg_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..9af0c61630e7cd7403b958466f620bf07b419162 GIT binary patch literal 106 zcmeAS@N?(olHy`uVBq!ia0vp^96&6^!3HF|1ZAaxl%c1KV@SoVJ>RoYlU7$y7#S-2rj-Wy_hG`ec3;ew_b}>qD9h!|fWw8|syPpMjbgJYD@<);T3K F0RVmfAhiGh literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_progress_primary_holo_light.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_progress_primary_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..279e23496c78b2b68c800c8150fd9b1f5be1063d GIT binary patch literal 491 zcmV4!_!?OL@Tl1$Ry(jSl{Ns=T< zl4O!ep5D)M&wbf#XR|hoZoHhkXZL*Gw|PF#5m_JR(a=xB19)?UVgB$x^q;~oU00Y+ zihCqgvd@*8gz*&c6w&!HA~iv?6onQcL#={!9+lryD-Pp(Dt|Gz&@Q#HZE9y}>R?;c z$xP~k7&E8`df6sv&<}A*V=3wu_hHVmVdO8MmJDDQw+r)$-J)@pqydZ#VQd6OF+Ps* z1WdrBIHqV!1jGY4VQ}$OIui^!7fiaq`6XPzHQc~0+`&COz#}}tGrYhnyurJagAe$G zy!0h~ODENYsU}daL?C|{7KRB3lXt@yH0?7Zm^2G>Fb@m3X%UuS8CGBw)?ghrAPFX< zVFxmpXAkyq{{dui?FfzqlMcmril*wZn)ovAbCz(L1~EPyu%Z&;I$wd9ufjf4)Im%2 znyAy%cu2d~SV3dHpqju#@NsJnT}DYd41Q}_xI hz5|D0%A{&E`Tm ruSZdh%72r^kN(LJR$6rS;p02Twp<}8vnzK4ZD8bP0l+XkK>&Pz3 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_progressbar_indeterminate_holo1.png b/flock/src/main/res/drawable-hdpi/flocktheme_progressbar_indeterminate_holo1.png new file mode 100755 index 0000000000000000000000000000000000000000..c5528d387e71f992664806c4d456029ab19df22b GIT binary patch literal 723 zcmeAS@N?(olHy`uVBq!ia0y~yVD1I7B{c7Pb`W8E zVDC61VAqRP;x8PzZ2k2Esv6Q_tL8cJU9v6-bTT}4(n(CJf2kVl$;sF5vnxqRyV@AO zKRbEt#jA&Nfkq}=Dt*liq#r!JWWvP2VBme3gMs0Ij;}IMY_kc->NBtSq8Bx;dE34J z_p80Hdp-)8Tj(bXM?}r^KeZ?5nXo16vX!TE*UJ8{y=2~Few0u3sNsXd+pKdAd+s=Q zBd1$>PX9(uot5)4-gf0)cxtj<aO7hyDD~Mq(57 zPO82Q%_I*BO_qZkdgy!s?O4fks(b3lb4*%Qkxh;0m`I-B=%2vM0 z`|JJnpIYtWznYKOo~Pt6d9PZ3;lAq6d3BGrp1xsF=kdvIdHCyxmmn@&yzI-Xhi3fT zGGG3`iJdfGa`*SyD*2QCWR$r$B;_!j{xz-c(pT-jQ@{F7y8cocBonNlEeQVYqRVQFTrM>(&KWx2A$oe&G zTM~gzk$QJ`<@?e#(>Jh#LrChs-pT7bEVqW-52>1eS-3#Hu({aStWnihx5ZeeDR`Yn zMfs0CtpyKe?7DO9)<^qDj_0{L&C#=>_LWJWW_Qj8TDxVXdmhhZz5Y85_QBI{M_K<< z@4vId{*Q3@`6_=~jpFbczdH|VdVCW+e?Q8f8C-Vtx3hBOGvP|sD^IUo>-&Fk-|n8% zo2(j-ze<|)>T7`#FqlsRgE{QkWTW*wlMRGpXMp@t_UF6jf77MXA^+oQgBqqE1gbE( o>cIr|XT+b<{9 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_progressbar_indeterminate_holo2.png b/flock/src/main/res/drawable-hdpi/flocktheme_progressbar_indeterminate_holo2.png new file mode 100755 index 0000000000000000000000000000000000000000..13c7c183b43382b494373191330a00576412a1a9 GIT binary patch literal 806 zcmV+>1KIqEP))MT)6N$g6C29;sspm83Yd?u0(L@!i%^NSHln_x|l9fsp&GAOzdgM z_d%0P{tV$2_4>U|y86MNHvs?ufJtZf{Q&>~09Y%Qr#w-_?|_qVQK0)yKbJV=d8I4>>{5)pE`LS$Bgqjxo#LAR&O*= zhiP{ZzpJens?AOVmSNp&W!{!KeU_W^R-Oc=$R{vOoji|Y$9XHC4M{F?-8i;y=61^9 ztdd-fx8?D&qj)%O=h?&?xIcAbT_w33A1?`Z$n|`^$o9N7cY)Tv`J~hW@(HIzoji|Y z#<}FQRg%?^#`#;4PdPl!^?|UfA<4`C_~B30ha{h~>F&!QJx%ib zzCIMN)B7a9vPrz>NghX@1a7b7llq&+i^wOSPM*i3BwwxI>No!KByfl}pVW&F!YNZH&!dj(o#Sk90002!a>VQZXjf1xzXxuY-UgPX%+q=W zvfure19Y|dq+P`jPL(=&9(7#r9Oog)+c+WnuBJ}#y~_3u;ws74@xBv$VHw(mD}Ii* zW!{#@-o`>Wz`ynKNnImcM%~5fQ76x1Y9XAv#CdBWoCkm2qVHtK^jJux0 zF2)V~%Wd+Bx{FhyPM*iHp2xv)*2x0^0MMmr1ONb_OVbDd06>?f k5dZ*yE=?l<003Qo0phwKV=&{ zB*(yb+ss@Rc0WvI!JC{y=BAlgw?i|j4PcC30o3#amZA+!_tjKvb6Jncb=TvZZE$Kk zJo~wAlIJ-E$FlI_?yFn*Wh+gx6C zyH@QL#x~!T3y44D^2E(q;C%M*HV+dk`8vtJIVIBDBrlWv_1XRmuGKe5{?2)Ae3jz{ z2vC$>QLZl7k3iq_1eW6098Xq!$SE|w7@m9m1%I#q+f=^$GslxSQ})|OUaZgf)c=RE z&w8s3pw;#Y`w{4yp1@L+W7qm>;u=1V`jj@`%|3=-`x^rQ0Kg}BELpar>$D>b(XUb$ z^+;4|18B9q!hQt$rYEoz<=C|yS+uzsDpuFk^40^WcFBXxa9`)ZEmlyAAvsT2`tIjEjP+ZjGk_}F|`dodCQHvVF8(WBYiW^Q#S^!Ys1ZXp*eZWJK*EBnzIeo zw;ids!Ly&+<}~DL9UAMEv@5-D=BFQO1DHdv0BU*yOVWdE_;0RRAiQLBgk0ssI2JYJGU0000cNg4qF z0GK3c1ONbFlB5v;0Dwtdza+;ckT?C3>Tms7t0O;`^osJKi~mViTPH(=Rrnx)kL(l)H}as9vK4nFs1 z?(t=xy3^L5^1e-%07zFcGcX-!HuKVXp$G*$ZZ#cAn3v-Syb=|4+x3RiY_N}a6cS*j>c*eYIgJq+y zll<9lc$4AB{5;9?e0-z_igIM59O-zQ6q_i7)LL#_yZ;weUz5DbHVD*}^NS>3J>d5O9smnqQLqmk-aDJj;3dw=CNy z*`64y%hsIY%MTg)+{tjj zB-U3Y!71jRm1&{g|UHf<`iGrfpD(#VbWMeBmOxf1K~V*|1zKEJX3Cz zjb8e2wA`<;p^Z!9Z^{i|%&vAr41)O@n{uNb%)^u$oMO)LWo*igsk$>d<;HdYrhPNz zT@zoH^(B$Kw23ddKl_%YvQAk7#2{Fn(H@VwmJX+002ovPDHLkV1g5d+%*6I literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_progressbar_indeterminate_holo5.png b/flock/src/main/res/drawable-hdpi/flocktheme_progressbar_indeterminate_holo5.png new file mode 100755 index 0000000000000000000000000000000000000000..78c7c8b1e78bd19920908bc1dfe328d3d6e484c7 GIT binary patch literal 837 zcmV-L1G@Z)P)eK}Q001yuLL&eG0KJ4p0001b35@^%0Q3?X z0RRB#Rc(*txB>E}_;}NLZB=}2A(km4`VrV=J%MF2;|#HsKIzMA<$0t^@;IB| z*glzuEMlGHYI~$8qDpeU{rbnM0cv$GUT4#b*Tv^o+4N%FkLPLn5uok1^`yQiYdwK^ zGUE)fgg)uZYe^9tt0eEzoq!*@h;@?pt=G@939WCBSOX-@)u6f;e<%5prbk!V4D`z+ z&obVv+ra!BT2Iz#{*&-L$wl_>rU9nSj59>LPktr&n)&rfUtZg}MYT%uX2haCW|P3n zcN!m$TmvM@=Oo|L^kY5PFOs}EVO_xdlwtMP;|x*u#hm-3GoMvSK4rX(a4xeS8ZVDr z10>zQ)P9wwAL|{Xz9sn+8{q)`4y`Bk58A6FKevCL>gESPH#5!&A5-;Xm5n$ksFMqYg^>&c;p6fcekx4bJ%7b=x4?mVhMe6D6j22a^rIQ;xUqM z-@_uyWM2QfW7Po3?GIorUteds4`2mOZ|HhbUvajcz`U4ohFD6U^yRgkTU4tw4{$tY z@$GMFyU4nzDSZQ3uCfoyeyj(?g$rOP>j^BI8E1$k^hsY{OR@=#r{0Lh<|9l+*RTWJ zhxG)O&5Sd|V)|t2i5CC>fD;lL0RRB#B{Tv60MJWl1ONb_m(U0R06?#QHL83FN9jl0 P00000NkvXXu0mjfNX?9_ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_progressbar_indeterminate_holo6.png b/flock/src/main/res/drawable-hdpi/flocktheme_progressbar_indeterminate_holo6.png new file mode 100755 index 0000000000000000000000000000000000000000..9e1ec21c31c87c3f24c91f317cadee074e9a76ac GIT binary patch literal 955 zcmV;s14R6ZP)5Hd-ZEW1^ZP+FOwTNQY}HhKRoz|n=;gBj006+G&C&q?007V~gGK-V0A?99 z0ssIo%b*be0DxHrjQ{`u%$dyUWo8GbnLC3%{|*;8m$nLR2qHNPbJK0|%qWM8*GNb*kFyk#6{ zOWkK~lYE`zuj=!w>?edj%kup@aOkewK;C=4#^asulKftsD{c43N#4%w0pqE!G?u#g zMLFLk`Se6Vysn=!T(T`co~KDZSu!YiL*FL(G5bWX?ik)r^3j5E0qgZml7DmiO5>BX z+rI<1)ww|;@4XbiCHXUli)94-_3s=Xv|bjAn!eYtR0~|MA28$;OL9+bxl#WUEj`Ry%`LBN%MIZFnA&oqBp!(^H((8ppARZsr020DxHrjQ{`u%ra;M003Z?K_dVF0J97l d0RRA)^&jFhg4$HK1$O`d002ovPDHLkV1g)*(^&uj literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_progressbar_indeterminate_holo7.png b/flock/src/main/res/drawable-hdpi/flocktheme_progressbar_indeterminate_holo7.png new file mode 100755 index 0000000000000000000000000000000000000000..eefc7824377f21b418457ed755600f59f9573f8c GIT binary patch literal 755 zcmeAS@N?(olHy`uVBq!ia0y~yVD1I7B{9Q7kcv5P@A_ulc939u zV1BS=LTI?9Lv_=-s#S6ezA&6zH{qL2t6p_S+0{^SMeSs#j@=z1{>R0BDs~-vROz6x ziD%x$AG1HIyDo12S_w2aVfo>e+(6o4w+hHIo=GN53=9SympB+04k&mk1BIH!?oN8+ z&Quw_CVzEp`^_tf@~@bZh4l~I_+os4@1q63?$*b(l472H-Aq$hGf(Djy;I^{ufV_A zvmtstcaQHLhUClV9juQ$w%Ky2uk=#Yb>jjlXY+0cdmAS zUD~*D>c^tFDL41V9Y5{4T}=N#uWv+BvzjsLga z+PLr5Pvw)}fxf?{|HpCt!`MlybBsPbR>`0AXVw~*3+wnj>z@CwsGR-ed5S92vt{qo ze`(7cOP$eIF{k~z=f9L+Wq+nU$@%!;_N|b#>rc%#thE!q`rqH?fcMJZK)Zb0ZO%Ds zzpa>&@^F(0DD1v{UE^#!J77O=&g*QKo;nzwObzN?z$PQf9ap^ z#;?5#Pv@xjo_^yVn~?LB_v7@tyWVH~x~`X`B=exM4eSp)us@2YX5K2i?qabT=mWpY z|GwJpG)*b(5d@NOBKG4zAKFyume@oKSm$tg1t)XdQSnBF?hQAxvXb7fl~@ljM{JPK|Sp zWVLmwv~m1ia_#v4vp+Wh004kq=g<8Q0000mT|y%O004u8MgRZ+1__M-000aU8UX+R z7}T^&GVcJnY?oADll5ah&%7T$98+Cu%g$|et*JR+U-}G;M}1A(v2)EbSp~)y)t%2N zJau>Y-_=qy)uIUCX_lR()LW^=^E|A#IrSW1eEJNGM}1A#bL5(heW6KmnN@J?{((6t z8+DQ7zFo@tvg5yKmy4{o0kFHeku8$kwd+emYqF}d%WT%0ngh1AZJ#Nx?fMLCPkl{W zH`gqx^<(6kvOg@6ESe-A(w|EIcikf;Uz2>vY9s&7dc(d>a-D}qO@Q6hHMdCeG0BhW z_KPG}N#3^Kt2ba*+w_^sB=0lN_ckyl^)+4POk+8__&v$ztaHv7xu(uh_f3*_d*0Fi z=v&sO*~`YgPV(kC|0$TMPf31d-RLfoyvk|y9AI3f&(vo^E~C%DA*iqEdYRS9u==?f zGuJGd$A4!w%4yp=b^ACfjHmB2^{3Qxfbn(vOg+FK@HFacx{jV}1SS9g00y1X(Sq`s z-0roG-Jyhm!>H>mS=SuEp>+F9cQip@f9h+xj-G3pB)6I8#jJe;;gmse{$14Bu1^ev z15eexfc|IuwYaIhhDK~(p+P2S_5&(9mzNW2j%8e50k22-PUHgaUcGUl| zNifUpB}g7(5==N{k4*|(*Pcm%fqm@KXXg0c2F9emrt3L!&BngaWIf^ioS*RSbyY># z*mih0oNi-F=jYPk)EuxceFny(zNYQixn`NHPFtSx1+9G>92igc1qk5K^cfhB`kJm$ zbB!Ye0000C5*h&j02m}R0ssIoNN5BA0AP^N2mk=UpuhKQWqk70v;P19002ovPDHLk FV1g{El$`(o literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_scrubber_control_disabled_holo.png b/flock/src/main/res/drawable-hdpi/flocktheme_scrubber_control_disabled_holo.png new file mode 100755 index 0000000000000000000000000000000000000000..12006ef7ad4b8c02def6b081ab1c7f8d41c115cf GIT binary patch literal 851 zcmV-Z1FZasP)}A-p}mMmDIy{w z5<-w5yUVtRHw(e;%Xj9bg4`LblO1LPW?FaHEvE z_qD-GfY!Q=7`}>lJaD(L5(7iv0ufR=yh;nuTK9nGz%u@ZKO{s(6pG6;dYQolu^8wSrD$uLRu%ZOO8ZsKW54u=F1l0AlDMJexg_i3ft6|3bD?Coz z+vt%q3NFtsBC$A28bDubZJps=0=mrB`zbvfc4P`L;nTJK|H!nFT+GB~V8hkukqPh= zP-r5_B3TH6o#sLBJ?hO{KD^#(9`urhpvq{rZFyizj+W!UTwXOkcDJJAcfdxAh^^@O zlqUN}(eA*r%N7=A+q_aK_FqJww~%#H%Uql0o-bB={)Qy2giS!YhxOuHbNdT$f~@mH z;KbbX#cGQ!&MOHxjQ55#+24weUxqZ}@6GM6hw^28jdITloenYB4IRZ1~}|+3AhKY zsgTSy5CYdGA~LZGFpaL5Z7$p*BVffjktwC_fJ;&CcXr2YB8xeF>sFL zp_5zzrU3hcoT03dG;}Xrv7b_CEo3TXrxfr=MH1x0aaOc)U8*)ZQQ(?JnI%R%e^#bi z?Ni+C0MC$$h4-G}0b`^>eNpzzz_+~KK+Xi#sJ3?wkx%6XFeno952<><(MC!yPmq19 zb>}oef=rSFB;IeU96Qx=MrNvK9Yn+uvY(z5lNy~KF^zDnFw0QuAs_s4(3g^=mrt0? dnKS1<<~NT=?++WwhbRC5002ovPDHLkV1oUIih}?E literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_scrubber_control_focused_holo.png b/flock/src/main/res/drawable-hdpi/flocktheme_scrubber_control_focused_holo.png new file mode 100755 index 0000000000000000000000000000000000000000..2bafa93e415874d25575d840e9c9259779246d0f GIT binary patch literal 992 zcmV<610Vc}P)cyGk zOw*m+GuaKX;15eS$G6hZ%@<)j_)XoGEGzYGMUx<*B1Q96$)a!HL zF(RUG7R#K?1tRDQiOMZT5mo_8{uDTK_d_B>9HVn*iOi2q5f%@I8R6z@R`a^NNK8W9oNJ_nuwFMtogXCx{XOx#t_AUqa< z+%>X}{SiX`E#B+f$PgiK-1kpSgd(>|Pbn2596 z1f-N17m0+45j?ugdE$@*~SU>6rBav zy+2|NQgE=%fmp{W3r>lYzo}Rvx!r=A$Ywxn_ds8`E!ZH7RLZMh?b>XN&qrT1V*+&N zz-s&pvcg(%<=R;LD2cpkwh6F$M+vn`vjow2n0+nl2 zkXBMuzz>AvnEvH-O~)=pOhD}#UE?>cQlJs+iyK5hbg!swd@G4%tnr>vAr6cVALtga zL3YfB@MbDjh=A5juC5HDQMe*4t@gz_=(9EYT>^AXSRxbCy7c!!;rd!**eA7=%oTn@ z!+v+?)(Cm6lRD>~wdN1!Lxn6&~74 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_scrubber_control_normal_holo.png b/flock/src/main/res/drawable-hdpi/flocktheme_scrubber_control_normal_holo.png new file mode 100755 index 0000000000000000000000000000000000000000..606650de9e6ebe5bb220bf5a0fd7724e4b5c567a GIT binary patch literal 1244 zcmV<21S9*2P)le(9HY}95aVR zk^j2ebdmD+s9*UIo50V=T{~pycnU=XTGsIavV5U{8nFTVh~jReNQYSjn%3bq7jOAI zHi7SfZR8PC`sV-Pu5%q(zpj9-f*x|EE6kXMJb@-+eFwN!yK3doKx@vID5*5_Uxv?X zDC^wJzj(=LBco2uLY}||Di1mrrdoOQP%Nww=p7PhA%@$;l?NewHoaUpH)tf5eiJEv7m)}p-Z9xb zs6^;fq2dg%jttW``Kc3VlGXOH)#VP#+3h*+bnG2a30-w2T8MzVWa`R#{{Oh5Kt~6a z*hR{RXxqCniNalU5>rl~X@birFiYqS6I>P^Co!|_Th?;~e8UnWu!VxZN?)%!W8&>2 zg6(Q}au7?vybP8Z`y2s330Sy&D?ozsd{y|qH)gy^zwp>&M4Z4e zTBNFwPp!{5V!XEm#tsSifm2naWGif046RShj1LL6m`=dm;7x=;(B}II1?c6OV&uXcqoYNP*|T3v!2Cc>*2>Pl?OXbzz8f^7GOO^2>Q@FYhD9wOtKW zKmz=M62K`ron{g6VBSLkd6`IozktW&g4Z#tpi0OBW*5!KW#JX@h%CKNc|=h~n2@cm zyNJ+5;}Cd=l1gFWSClH$gfe~ zJ|aOr{|H%ruJW*Wu?k6yP2hdtE8@zHdE*RGwt0+v{*)4_yv%Cr?&Q}!1C-M4po6Am zj^BOpe2(0uABvxj z{YHLF<|dUap2xCx0PYU{JBUWE<=@S>rLoFOjFl->1@8fT)>vtzh2OjY0000K*=J@Hzpy!H@3Zz= zzu#~D?(1B2=_|GZ9gLOFL3y^g6$_1R(mdO-1AB2Z_F$sSKPcCWcmdDgIb6iTMi<@) z16?8Z4!j3%!K5&=tzrB?2%Z%Nj^da2EzY4o3K3V0ph8{j$47A=Zp3)QxM6}i!A#h4m7( zUKJY0J@_ncmtBB)k=j!@iN6(Mnxt46_BLFH*I^%SD9;X*XLsYvqGE%Fd6r;3JJ7>J zh4{(xY(cjE2>v9}Z;NVI-hhK*PnFRh7bYIDhM`~m#k*ErWCtED#P2MVKaU^b5KbH2 zJzgrroyD{GC$7cIL{RS%!JNQLqcG4b#D5B}5VbfV26qDg8jZ$ zi+wa2cavuzI{y$pDU2-QFuq+DT2nViz#@K+Z;5&iunS+n2UeoB>0w6<_A?@hz8KUG zweSubz-jSBC(Hc}_#AF&8r;OdUeWp8<^C!BSbX+>3uo|sOqcru__S!l5-ew+D}Mg= za(`9~_WYKRxjIgZqnH!#bib(55-ew6zXYkSc!)pWwn3*5e}5r%r2x4b4-^90s?@aDCh?X#s!7RqS4GJM?Gv;;61}eoG!5vL zvDdd;;~%1mL}MnY8K`n{Wl$RiI->Kt)*3j3$?{H{UFfSB*k*`ESJwtzLn<_;R-b|P>eDskms?H#7uEdOX9f)DtE~|RQz+cP5DuTAzg#{_>+XSaUd8duNjR|elnlsdd zo*kO3>Zq+3Rl(CHk4YQkoMy=m%5Oiw?^YPI8m82OeNwTzN>ye=4Gxw;`=r&; zW|2kwMugFn%$*XoIWq$B6-`yD|CkaJDh3wub9_v;=@$H|FxCbGDFx4zMJundSQZ|& zINGC5_0LK}7)!m)VM%vM()Gp>7bAqui2!EG_fd)ON~YW^b37`$iKPsj$Mb^#HVL@u)lJId!H}1y! z#n!Ga^Zf*$7s;L9$z!v+#z4~d^YszqHs z%0V|tG48~N@D8aV+D7bo*|A?pdT_)DhbW^mP@yL-_MmvH{px~f%+mi#ET}7~V^R|w zmO5nQ|4eN3&mOa~JUaf8OY(;F=zg38I=Y`1QYmD7A{0FMQ VoaijR(Bl9A002ovPDHLkV1gOHF{}Up literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_scrubber_primary_holo.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_scrubber_primary_holo.9.png new file mode 100755 index 0000000000000000000000000000000000000000..bdd3c992c2ae033e268879a12d80b504eb94046e GIT binary patch literal 143 zcmeAS@N?(olHy`uVBq!ia0vp^ffrITPP3xM0EFb9}2-N3quh0=-&%U*9?aBR3PTGBu!S6q^UK8O~{Q6(33ups_r>mdKI;Vst0OL0`QUCw| literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_scrubber_secondary_holo.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_scrubber_secondary_holo.9.png new file mode 100755 index 0000000000000000000000000000000000000000..4148be2c986e7a0ce78d7361ff336348f074486e GIT binary patch literal 143 zcmeAS@N?(olHy`uVBq!ia0vp^fhi#852{T93vFU9^T*a`_j#8t^JwvPjDslEn%4Ap_L)>yrI&%oV8Gw^S4mfqc0Qx sSXXMLNJ`n>p0K{jO})=F`0EcQzc?FVdQ&MBb@0Q=!I^Z)<= literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_scrubber_track_holo_light.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_scrubber_track_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..90528b1307e3df0b32b2fe390fa8380181da5a42 GIT binary patch literal 169 zcmeAS@N?(olHy`uVBq!ia0vp^f%A znUur5fT!l%WflerN3~5W&dEJta5)mqYV>)2h3%!iO|GRTh5h0M3}W6jb*w=185lfW L{an^LB{Ts55H>O< literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_spinner_default_holo_light.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_spinner_default_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..03da08cc88cad4ccabcf6ee048ea178dfc58f512 GIT binary patch literal 421 zcmV;W0b2fvP)C3<0004ONklMMlHhA^osaomncGf zk&!4e5=BPh|H}A^Rqc6iGQMI6?D&fHg~|kxL*Nn^NO5Z0GKw4lH^5kn?X)vdNA7`5 zjF<0BCW!1Z>&QlvoKChW5_zjV+S2l}cSaF0>t&URL|%lD&t7CAktgKOccQJ4Wo9Cg zXJDv=if0UD=u8mV2ObR(v))`XL1YiO21X51x{(PY$G{!1-FBUrRx=Zc+|sNgP3pO1 znP^xafVI?ojihY~A}7FWZjmIJAaV*^rR1fcZc`BW;S9J4Ib&w0>Bv}P3Vem@0mcbz z&CGi(_QQy`m}o+l%)y++>rCDG?=pUg{wC3<00004b3#c}2nYxW zdh{zN0QM%_4OoDs@ms2sOBF{iL4U-_Z z8c0`+fxOf{rwb-Q-q%37U<~AS?f6YGA~FGQTOds_26EXtZWD}w%zMV%jY*IzJ1Wuy zlfE?zu_DFqPFIYG1Y$Lo77&1GBV-~@-C3<0005dNklp8;TI6ncy}74iG={WB?0 zwM(WpiU?3y^QunCq;-O+RWc2XRSk?y4UA0cYBxb}c}RkWJ~#C;1D>e0tEap=A7^ma7@}QyENDKhF&3BC3<0006yNklHV%7{;I1N>k?=(?~&uR!Kp?;;rf*fGWh)foyyWA;H1~3j;z-85j_%I&|pJksbdd zNLAG~NR`+akSr=WaZOZm4CE{q<<53$J13E6^2d9Ae}3NgPV$+c7LOcLOzgQ8CTO)< zH2@|6Bk!aU4FCj=;~aBdO3& z>O^5|%ViImom9H+2p19z zQRW-FOt$5+$IFN7mu7o!iLfy4bIuJD0cj_6L`dZfrG^ouxjftZ08%$(TBU-qEtfr6 zKDe`B`X94|NDX6KE_-@q|8Bi}@;VQYSQy)K!|mATSNERPW`4Z_p#%<}1N~vYsAnWc z9{@lo$F0Chn3p%cd=ke57^mDgIJ4=NW&`eT?BIJ)l>#YYHjh>xrAQa6;CE^8&7U6$ z!vU^@IrQp&mN0SB@>69nQHo)H0i*2l0u%r`{^H@1NlMDo@ubK_e*gfin6o-~*0}%x N002ovPDHLkV1ml26~O=i literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_switch_bg_disabled_holo_light.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_switch_bg_disabled_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..f111d823f217de9f085cfe851f8a9dab7b0033de GIT binary patch literal 249 zcmeAS@N?(olHy`uVBq!ia0vp^3P7yG!3HE*cI@g0QN>Q4Asj$Z!;#Vf4nJ zaCd?*qxs3xYk`8ZJzX3_B3j?xILLcgL4f7J(IWN>VXt<~TGv-HS*n0{cE9M+zRDwW za!=m++vs3@o_T_c`!BukrP_v`uX)SsWg;`u&aYQJerzYF1MfRMhEG+SSy!Ze>&huF z&tYskG09fYfq{`l07$Tv_-zwxn7{E&&Vi*+!G_a}YyUSI?SHNu{Esmur$$Xh($ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_switch_bg_focused_holo_light.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_switch_bg_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..8bc7a78cd692dab9716bd5c0376159947116244d GIT binary patch literal 217 zcmeAS@N?(olHy`uVBq!ia0vp^3P7yG!3HE*cI@g0QVTp?977^n-`+UL+oB-S@=$*B z)&%2kj@;7{xW6PC-{Oh8@#O8RgB=G_)f6u>pL_oHsbgf&!&(`KsE(!bRUvl5cF$^= zzq#F&lVH?IC^ydT{u(9iU=Gside%K@9;_^}8$Fff7%wzwHN-i+(a7i7c4 z8s0Tr?t78n9&CM%spekX){g&n5eH`YUyeE3ytVzx{Bz7k7NXl1?_RwR=vD?#S3j3^ HP6fUM literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_switch_bg_holo_light.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_switch_bg_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..7abe99a5f544eb878f28b136381a4f5f3e51a76e GIT binary patch literal 182 zcmeAS@N?(olHy`uVBq!ia0vp^3P7yG!3HE*cI@g0Qk(@Ik;M!Q+?^oIXnykaTA*Nt zr;B4qMC;pY8+jWHcw7P}|DAEzxRQ@;PYDMj>!jp6e_jin+!a1AQqN0PyVUPm%IbJb1%HV_Oe{~ zx#fP(2IdD6cbGOkD}A^!H(_Ua+{Irdym@<=tK#bg*JXDkHZNV);Kr39QhFe9*DKL8 z;-v?SI; z^RM_Y=b`KewX+uWHIGfsuCO?-CE@>~wx%$0b%7IvqEMPn^c)I$ztaD0e0sxw_z2g7? literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_switch_thumb_disabled_holo_light.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_switch_thumb_disabled_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..a257e26ddb5c97809bc7294030981211e6498fe7 GIT binary patch literal 535 zcmeAS@N?(olHy`uVBq!ia0vp^0YI$7!3HEheY6(`QY^(zo*^7SP{WbZ0pxQQctjR6 zFmQK*Fr)d&(`y+R7znGa>t&u!9Pto~&IFlHG%UHx3vIVCg!001%PSpWb4 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_switch_thumb_holo_light.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_switch_thumb_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..dd999d6de8993107502e939a4395f018188817af GIT binary patch literal 559 zcmeAS@N?(olHy`uVBq!ia0vp^0YI$7!3HEheY6(`QY^(zo*^7SP{WbZ0pxQQctjR6 zFmQK*Fr)d&(`y+R7%zIdIEG|2zP-J%@0No=!^4|j6Yu`&(5MlqY;(!XRBGM+NU%aT z=(4M(qep|wMx_S~Wo5Z(K`!cZ{T_DBmhQfMHay7S+4RU5p0i z8chw#@2Hy?-@Kbc_g zx*{fr0LJ0;Is$n+n7fBEH?;~6HCx-_cU#as)w-^{TqwUP}Ey{hk$^K{3CYf{f+ zqON~^emBpYz2Sb)W*6pl*I)b1dv3lrZvB#J^0S;c&erYs4_iI;#l=L``!+2J&K%p; zTE91RSt7Bx*Rad3rkkyX-$Y?DTgs&dAE^nR%to0GYP=_wFwP`i(1qE|m)X#*yJPM8 zXG)7Y4R0;Wba~$K?pVe9Lc96*ua&<2WV3#fisQ*j{&UYa-_6^ew=?G7$9vu9M0hP` zpY?MMJR{b9H2nFUy<6YTk?7zE4ZYfa_~G*J|5Pl!OY8+0e5R$AM^39XIcBj?^1rJ~ zXymlnn{@mYwzqm;Vw9gDmS*Fsuz%m0*B8C^=DK}YA9uaXZqKiF6{R2C!n=20@0l}w RDKN$vJYD@<);T3K0RVwo@BaV* literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_switch_thumb_pressed_holo_light.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_switch_thumb_pressed_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..9aaf03f66735d0edf244e210cbd3f183f9fa8940 GIT binary patch literal 469 zcmeAS@N?(olHy`uVBq!ia0vp^0YI$7!3HEheY6*6U|^i%>EalY(fIb3e-@LW$nlTU z4L-_m7OdhCw{_4rV*d~#ccA+V$6Zc22Xh?bNui9Hkf}tvbgV+x2bRP z-tU_~|NIl?wbJ|L^9|C;CnSWw{r+2h*+l;I(;(eNSqI*hSx2r7GgeyV#L+l2)c-Jl z_S&_tcPTB3yuB@3i7RSz2y@kY_QMZ16rEeT&+oo@pW(csb64}WOK0$Hn#G@*dfj-Tp$7Azv=a|VFujg++yt;VZHIPirn_YQ#x8*LkJbvMN+p1gNeplG6 zb60|SyQ}mWzIVN#6>~_bB73_O1d}m+XAM3Klo!Q3h?C$sX-mybP0l+XkKwtCC< literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_text_select_handle_left.png b/flock/src/main/res/drawable-hdpi/flocktheme_text_select_handle_left.png new file mode 100755 index 0000000000000000000000000000000000000000..218917b74dca647eb923eaea0284280304606b76 GIT binary patch literal 1356 zcmV-S1+)5zP)wkAT*!n zbFcf{hbQ;k18_JT&Xy2XflRCd835vWw!6UB?{5@s_PKxl_${t?LPml3`i3f$3!e9s z^R*ZE2T!_K1Ql2W(G!c!E_RNN*L>S{E&xaIVT8l8%W+vi^7f+H#NN?zTot>lZEs98 zPd1+(gSDU)gopsh>cV_V4v>YQohpay^&PN(JY3q|y8E*aV6k}s5GV?uC}bscTSW5Q zV%ZF`6XgiaJp=BaUY+TOOCEt>skrN}ib7)Hc?rpLlku=rj@e=nI$nRb=Hheb11Ko% zgURH8qTKx@N=bYSsbfev`NbvHuHcFKub%pJ9JZp) zP5X<+7*5WvI0ZSR>ATlN=?F_Ijiq5CE0jQ>U-N@jxF?k`#NJbR{`2SWx~U0B_9g&=k;MQx zkV8x+;(fl?d3EqwFWeV`FiTZnxI&;Mn#!?QZK7|$UorjSv>!#~Lq_f|nb1s*v+QxH z)gSbHv;RyGcIQFlr!_l=@3n=!DieE|uX$+CzMV0jYBMKnrxH-5({%t7Uz=3BFGvN00)3ln1pJv($l!;zy731s2ke%HVPnQ`NVr8 z{@1V9ltlbb0Ji`v0C-?gEaiv*>;PNxJ{wICrpqD`oxu3+pSI zN|L1goZ5`4Ro_X!DCyOcb>*<^Il$V>1V{=Iu`VVTw^d3|42*6o)^dRLji)IoS#Lz$ z1{CWRB&YqJ6 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_text_select_handle_middle.png b/flock/src/main/res/drawable-hdpi/flocktheme_text_select_handle_middle.png new file mode 100755 index 0000000000000000000000000000000000000000..184d0dffcd9f30aac491072839fba7d3fe70e767 GIT binary patch literal 1454 zcmb_c`#aMM9R8wQnS^SjMQA_UPA60G4~n58HW-w*XJ~n@Q~780i>NX6XjoPdml3!<TtS;Sm*%FWU zu?ddu$FbX~N0FZlKL4%U78Bd$`JJtZ5}}H2h5#@31DM8MkyF#i`m!=E@8*$3CExzs zC(UQtSby3x$HV7qxm^L|H$JlYs9>XPejv1@P9CAYW7gA-&0fE;}0)%wPI?k+F0P{Jrl>3LA)bt9$Z zWy9-7#rnTL25p4028{u;2x6|N`B?_TZdYc396f`Xk34c^Q>EdA{O%y=W%vEEy@7OI z*vhK@p>>*YMFDH*>Oxzo_O6BUtm*6=p$&#z4NRaUR!>J_*XP1uVBZ9o{R}hqGZKdv z-W%GKH+kCf{Ts8;f$U6p5pkeI1L+$-_*Pv1@MO)najj(V ztE+BpIy|4@)dk)`ILiI_Oofz(?-r-Ntgy$t7hlxw>fA(Cmo^S4A{^q@HH7k;zc-0Q z$1or0oJVXXscU`Q9{{c8XUZHW*XfiAHFW|})WNp@8(UD;z=I=FQl}8di&IIIF-r}D zge=a^4vZG3J917`ASKIka-#3%#w_wBo-OhT$uOLo(F={Lj?&Zv{SJ+&N+!V)6%QL+ z<>h2CK!w|EOz|JbZ6idT4AmvwKer$O+T8L^6-*0~Q|G3|a)u*hd_Src`W!li)=HV) zCvbWYGzvR*&I64ZUGqad2a^-(dU0+Hdx|$PLSuBBJiPze)THg=y@1=C4+t8S9oyFs zI5#L?yFAWmw)yI3pZ8V@M}po1bH@j0Za!BWF2>Gah3s@|st)GCOL21^jRgO;h$8IF zS+OS5cjlBwaCG3<_CQU;h-(oiqo_!;6s?Dy0KX~az#nWGwwOZ1T6%G|SwlH1Lssss z^}7|*Wa;}bwaVi!rcp`d&U85_!Z|AB0|rCu_#)tW2dsZC5vm1ukuKXmJ}=6-&AkYrEfKnE3<0Iqd5II|&nH z2~!+;*7$dW^Kw9#;+?jX75xhX>SCn%!oweo7d^@)h`M3PX-Yei-C1vILkWRWTD z(kEs(c@18RQs`cu%8KO^8%1PX`1VhYAIas?nHu_V$37*6REEoF4}$KedOLjsldc$Nf?R*q~~(E`(Mg!+p%5cOEPyOHQ)F literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_text_select_handle_right.png b/flock/src/main/res/drawable-hdpi/flocktheme_text_select_handle_right.png new file mode 100755 index 0000000000000000000000000000000000000000..800b99d62fe2c4d2e0050c28076c43f8e049fdaf GIT binary patch literal 1389 zcmb7^`9ISQ0LQ0Q^0c8Ym*osu*7E`IRvZ0F@$?A5eb9cu% z@gv1ml&XVs!e!J_NNV5YVfeQCS@7=2Y$+C-ALT#iwjHwMd+$~#-^BYxt$9v{k0cp6 zWR_pASUeE-k#f-cS^?Qm^cyAa?1yX>O?_+8oh3*2rhC%g&7GZl%aC+&gUz-EHHwd4 z->|A?lom{G>Zf5i$*4b#Mtiz3y&~CmFEENXGqnEXg$0RUPpZE8yc4`KdNc8&+1Amn zcfgIu(z4YS+5O}4zdRIF6|?tW;cXSP3^bEW^G1VmPvY$Zxj_(hjW^W4bqq$y>vjR& zfp1|88I+M0x5PlRue=U6x7yHsT1Lji?vd}wKJ8>sDRy{$XNP_Ma5;CWAvL$s!moD_ zN`vF^g>oy6V=kW}Nz&F2VIU6+NC{FBrlBm)jSjA~aYFE3zZmw*#8w`_*0$d}Njy;kG5YNB&-!I2Jf{{M ztJr)1KZ%p}Y1jNt<)WWKoH7|NfH;(SE`zcPr74wf3_#MABSzjO_g4I@D_>uP zy1L?sCsywU=)I8@iD@1t|MVh;clf46qx1pIu&EM$I2_2bHA|G0Ro}i99!dC-8K8f~ zHVsTRjZxtc?kqKq;fXU$P_QXxPDDX7%+-?bfVwoKD?m?Y7b9+@iC^g%EYCQpc>yVc zUfU-jhL_uGDOx|zgHZXx(;h6|X<E?p$)dfI*Xb_FQE7L-xlVy*{qK8fyHka#qrkB-k+KXTh+nsPyoV*` zkbZ92Lpik;o+!4lF9iq#8vfE@@r7|xAzVYx;KHW~?2(JG7hxBPD?$6?W|SYdLDO`= zPT+~Kxut7k*dibW81Xe;wg$%-p{W{g#oNVjvh7NM3*ZNkft>&nbQzE#p!P}xjnm6u zUkL0q+TL)yqbFEsPTV|?@yH2U)VJpI4~=~J0v3~2jJQeAw-RkBZ@trkX}d)5HvOGi z5p2fSS&cBCQ|{!@Mr+m2$miu~v(rWJ5D7&4kbHx7{QOg%=G=ZL$#!^adXAIDuyAX( plK1V>deDK>krNmCPZHvH74|-U%KuT`qq&hua978f1-`+IjYA_IRxfuKL z!@a7UT%V9*D%}4=#T@M$T~#IHcEv3JdGB>l%eI?>{sJ$ZR2nKP9^U{mmT*thbJ+!F zOz8|MznYR-CdBsV(m#K9kK;2=zPDX{#82pDkW$fmT?tbg-P7-vaMeT?q?$Zk978f1-`@1*YB1noxENwH z@B4ngm>q?Wr!UM+xaYOuMSzA<=D|~!OPlB0uJ1XKT5?3%EvRRTNngB7&qNVrO(189 zgs0)8B$cBcUNcYB#F$3RoGWr5{#M>L$$P;`>b*;*bi~B3d0ATVsz}MrXi|{UCRZ8t l7muD@KltnY!auQx`KD>|8I)$txdU_!gQu&X%Q~loCIGTgO4(sldL#t*labZ51q`M-3-ZG zGIP?MELws>RInOFs0x-sLF0pcSSUUivBeUIeQ3Ra*nr@bD&lHTABwj#$!_!^^ud9d zbN+Mw@B8ob?C8kQj?V7RL?W>xKdctWdXT(a`|ad=X=eRAS?A8ebzeb~_HZ>oW7UzOS)DSo7Iow>=#NA~;2>;($eDISF_NeaUXkqMV}=47 zDtJnwHk}&RM}gu62&8E?X|jDR=z}!mQ<+TW5a3wIGAzeHE(tRt&xtGx8V^OR1y)Hc zs9M7oktAvoV_#&LN~J;%*PvsO|gH3GS4YA>xsA<a`T7@Z=YGAGZlQV`m!^F+;pJr(ukq(TvTdhxot>Hl~nV5i##+C2h zw8&W9$g6|J==zVZu5~>H?p!!HbNK1{-_Kl;-(I<+cA?hY_1Yiraq5lo3-h~w=qz6? zPJQ&mJ*BR9_E67%+0r^Z(7G<4{NSb8>uo*v-k08o_YJh}TAaUaKWOP7ce@tp;e`W_ zDj()gRR>=Fb?NjHSH5|3et-S(?u&bVTN-)?&RR$Kex^)dghaP6UQrO!V1^o3*dEs6D=%Fj#fwR7>mB%d2m&&$W3 F{0G5%lraDR literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_textfield_disabled_holo_light.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_textfield_disabled_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..a77d66d990371079ac3626ee54891cf0fe6e35b0 GIT binary patch literal 1116 zcmbVLO=#p)9FJJU7K?Tfp^Gdr-5yqUlJ}CCNhWqIGs$E-(9}Ae#py*%lh;n@Bri=~ zJL!nHlp+c(dg(z53JN_q(4!(AbXkxU^son^dhi2EQFqsZ3x&Px?#pzhdMG^@NZv>O zzu)iw`QG%@mBE4Y0}R6qmM8QIT`$pBeDVqUy}tC%E?tI6sYYh-JZV}XVhRqPLu}cz z7ElFQ&g#;4XpCVVpLeS@QZpx28+*K!*zmCzP&C7gjmLpyFCxOup#|5^bBAAj&9SbN z=U&U0zzj50cPCmQnrTf{?bf25bGY%B*|Auq1Rf$58+#2uQsX?=;Z^BAxfVFKV?q}5 z+_6(NbDGs~h}aAdQZ~o}HVb(urDa*Z%!&X40f+(=Q&3hVQ3ZhQJ{(mGoq4sQ8(l4W z%5!x>0#y*2%_iTJcpNSWFqg|E9HN+_7O7~}Csv&Dqmdqijv_mB1L9(zO&F~?yiD>O z_4GIdFEGs$Vn6C8il$76tw4Z05Iir5t79FJ3i{WLQ>~-wYJh|aituu1(|XL0^uRQC zXA32U)Eo7c(4|GO8alR@J>-+Jp6BQv-fUD|rFv&zRV_po!C@8S zMy~^=>zLrEjssSk$*`tnyMA&`%5xm8jzV_@IYx**wv%7gJw;zB$wo%hB@rIST{IL} zltrUh1Q{Ks0oTPk|0ibxbw)^<<3G*PyP_SKY)@LB9!`b_`E+7JIvT&-Y~QA1wNcg! z)%fQ-KNSY9fv5Ca8}W08Z{HZYyR$v;PF>Ip% literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/flocktheme_textfield_focused_holo_light.9.png b/flock/src/main/res/drawable-hdpi/flocktheme_textfield_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..b20d0260ee032c5547990b9a5a5b4090f8590c2b GIT binary patch literal 288 zcmeAS@N?(olHy`uVBq!ia0vp^nm}yC!3HD`7Jga>q@H`aIEG|2zMbjF*JL2#viw-9 zqTb#W*8K<0Sy!<+$9#;rjVGuCuO!UMctA4X32lyW(&3)`3SG`{%q5Y1KrpKgK zl~#eD7y6u?C+Vo1bV4N-Nq9=XojNlqt?2uX`sB68I2~Hg`)*;FRdBdlcHWF++mr8G z)6>tW2bVbY{k|LPdaQTrc~#5flizHMb^oQOXsRyq@qx_pl5JAcx4N`xB&l^z@QHl) z`<2*%_UbW18|ziM@ZOPx#32;bRa{vGf6951U69E94oEQKA00(qQO+^RZ1{?wrI_Ey*v;Y7A8FWQhbVF}# zZDnqB07G(RVRU6=Aa`kWXdp*PO;A^X4i^9b0$fQ%K~z}7?bxwz6lV|x@ZYQ#3L1+d zZoplTfXWSt8;C@a6fPsBNI^*n1&V@Gp)|fΝ=&tE}BI zdGc4Rh*;$L%Ypb!Ej){OS$ABK^EEq8d~wyu>C2|C6??)@oVjY^?B)FYo1(T`^91Ca z6T)wp(!RHAy;J$_)@lDf-?040s`b^aK4|$~<=a0`d-H4~mKRpNebE=c(}qWxt`)OZ zqG~u5EN2>zLCx#j?_*66wJUSoKAhgVP7`fu>0y7;ceA*FCJve8m5$rxlng=5F5nryC< zRsW8<_%6c^mvV=33{o&r;N3qv#`Pao7vIwvEd>c#NRc}Ei#|!JH>fVY`(TkhCYOR} zk$i}I?D=KcuZrL+XOBoQ!a!nC+}Q2fk(% zvsCFwGTB+ktA@WYgQj#|c$91v^%nJyCzjncS}HS4;X$M_`8%qH^SIv@w<+AhN*pY~ zu?Ph|OTLd(wx?exr6nu~S0a?sB{Ap&d4FGyY^k|`rD%uqsPHAO2VZ~GU$0L;ncrkM hio0k?OU10s{{iA>_F)m18)yIk002ovPDHLkV1lMuSGWKG literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-hdpi/icon_card.png b/flock/src/main/res/drawable-hdpi/icon_card.png new file mode 100644 index 0000000000000000000000000000000000000000..940ef6900aa513d9fcd81e2870e8de3b0b2318a0 GIT binary patch literal 816 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 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/alert_warning_light.png b/flock/src/main/res/drawable-mdpi/alert_warning_light.png new file mode 100644 index 0000000000000000000000000000000000000000..a39f97d4ae7cb75ea89dd4f0b747cf285174bde1 GIT binary patch literal 1362 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz$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(#^lGsViq$=TJ&!pY3k$-vFf(ACh=#L&{!#L3yw z&D7A`#K{n**Cju>G&eP`1g19yq1POzUQlAlEdbi=l3J8mmYU*Ll%J~r_Ow+dZnv1= zG!Lpb1-DyFaq86vIz}H9wMbD769T3m5EGtofgE_!Pt60S_ab1z{{3gSF#`kR5l9ku9 zT7rXLUX~1=zr8`pg4I@jqwO)V=c)<^?X+vW^o=EMKe%y$>72vKPq#Q{Tg3RspB6ga zCNzQddP2^}?z8IbWuAR@n|m$IUhi66Su^Qwv-F3z1rt^-J6SS&M(78gd1-2m2#f0elF{r5}E+$0qRl! literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/bitcoin_logo.png b/flock/src/main/res/drawable-mdpi/bitcoin_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..cf55a4be0763d5848cd22ca96fb312b2f64e5731 GIT binary patch literal 13005 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%0Diu4UcOu?1#y0Zy<;atSo9o=_a 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 ZfYNbQ78@NXMD+^-s?T34R?3?P{|^mu)`$QA literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/content_copy.png b/flock/src/main/res/drawable-mdpi/content_copy.png new file mode 100644 index 0000000000000000000000000000000000000000..efb2445f0a3e2ac7ae3cca3891895d34e1eb2b95 GIT binary patch literal 1321 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz$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(bCPt&DG7+$-u?X(ACh=#L&{!#L3yw z&D7A`#K{n**Cju>G&eP`1g19yuGh@~r(RHE$SnZc?2=lPS(cjOR+OKs0QR(1CRVoq z9pL8TjMF@*-V~f}adpM1S0CsYeNfaQMKw$an0`P^c)|s8;7LC<518JIfC>B1(*$M) z2FBT*E{-7;x88)G^*Zbzkhf{&+KF7Owv`$$w4XTENZ7tQdi;apmvC! ztessOoGlLSTFTFu>Fr?^UUBsE^omcnst+laspUH_IPfg})Qk=DX1n>s2bvpw8(i`siRduQIC$tl{|rt0gm?8$TChdcS1rn%fx z_;7PN(`=@^#$Soqj>~rH#vXIH=+?TznMrY7y4JB{wOR+x`LqW$G=G@*;g`A9BDY_6 z!^9S}zE~8;@!gJ#Wrx$8)Bb_ay(cpBc)i_sK<58_*NdNm7C7uP&)zj#;Om2D>ir6? z{^Ct1L|j(wOH#-$(T{#q-t+jvl*c<(XJ;fgB|p5g(X)49(gWuOVzv$0?;;pm`5E7} pth2Qbk-O&iuYL}n?*s1!Mh2^WiB8;)*Ixn^UY@RgF6*2UngHK1)U*Hq literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/content_discard.png b/flock/src/main/res/drawable-mdpi/content_discard.png new file mode 100644 index 0000000000000000000000000000000000000000..cedb1085b85cf28fec0152168da61defb93fd6fa GIT binary patch literal 1359 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz$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$->3i)zr|`$-u?X(ACh=#L&{!#L3yw z&D7A`#K{n**Cju>G&eP`1g19yq1OnfUQlAlEdbi=l3J8mmYU*Ll%J~r_Ow+dPPdr2 z;WQ7bHwCv_3~=hz2RcR{6tzfE4HE*U9}p9saDg0n(of9;ruQOX!d}0Wsg!|%@t~)R zV@SoVH`6zIF*^#hy?=C6g-guwsFP#h8kgDHTg9ZM<&U^+5L)9>BXHj#C&z`mTO&zx zT9ae*f(6sxsFwfZ-v2N{FC{-;wN>(m#iln#|IeK}mvhIWZ?0q1LCFogtQCLNe(YP% z=i4ayK#Yce2gqtnB)xR0HN`4y-a-l?twJ8cGA$`yVt0o>EPi&ZH#tDLU}k$&Few z*s>G4nfyL+MJx%P_$Xj2-$b)hTX*(9n3O1+e&R7pR^E~c6XKJCfxTz9hNZ&+UUN8KCg4=bPQ&9;3uO;UDdhwy^~Z4DE2 zHavK7t*grZ`7S1c&zvGKR= zFt4=+eGLWz3OQwmFFfFIn$5(-%-HD5c*vn)gKokC%MZsM1-(%|@>Hfjd(B?X{D|A; zKVE1A*UTx9+30>ksOG5YHI@_Z$1HawOkcgpd77|)(ae(fyKEOoZ+cjNxL*H(djlf_ XdqiO|XOH7mP!Z?p>gTe~DWM4f=>_8H literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/content_edit.png b/flock/src/main/res/drawable-mdpi/content_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..f09b2e4c20f7c13c8663eacbec2ebe4d03a0d374 GIT binary patch literal 1506 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz$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+0fY3$jHFd$-u?X(ACh=#L&{!#L3yw z&D7A`#K{n**Cju>G&eP`1g19yq1O15=2n zi(^Q|tvA>GJwgISj(;@&w^E0zm#3@CLu1L31A>R6TwDU#8n;{=D1tTwXLgX)mq%-zt{hO-|V4?-a~G^6%ywkTgxSK z#@+IMF!O-i8;Aa;-p5MU7RYuNvaDa?&iZna12^B}9cgh3A8n4D7Z`Z&aG2;b)0)q( zTEzY^)UCP{{#fSKg|*9ynVc)uUN1bfTD1M(;Z=NJZg#!dGVQ-wSQ4w%$D7&u9-=lf zmuK`ROB_z_$@+HlEo&tUzmj&~;{TMEzq0SHyZ_rKV{Y!=rt>S#ZcEaimvz!xOM=s=xTgfBEG{{T{4m aU}l&zzc+V_uIF-4sq5+L=d#Wzp$Pzkv^w+v literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/content_new.png b/flock/src/main/res/drawable-mdpi/content_new.png new file mode 100644 index 0000000000000000000000000000000000000000..884c9d2703775305168d0e36579816ca95140fc1 GIT binary patch literal 1099 zcmaJ=O-K|`9G|svOH%r{RAfEJ4j$V1*mji}-E?5kkOdXd7X@{whzN=f8FYytf-ZqzZ_Kq0Z3A!Kdw>4F-~a3Pj(2yp zS66MSqA035)gh(H4!pIplKh=w@dw#9;zSnrqJCUZY)C~ll!G8;C<8DJ6>a#yGuT2= z%eLv6EY8ZiL=_o~;`uPHVG%S%wY0jHq7Fg~a&SO5BlNes_i3PO5qeKp=42}l^Lj_o zhP}nEj9MI21&waq30ho{5Eu|Ez%}-pj_5|{1zwSyJu^##1s6ORp?@cpmAgS4*${*o zE}(LaJZKCte25c5!gdhkc#h?REY}p^IWgQMay(f4XcEoV`o*-AT#Q9-5ju~tC9-Ux zP+$r>7-SEyydVfH7i5FM0PzSo!zNbTfa%ni7$oSZwr*h^nZRRIa%c!gXp-sg5)5mJ z)^rxzL>k7ripBB_=ap1)l;!`s8pe{hgVXR&zW*e4GQ$>R)6hXfwn_%qU+;yo#JCL= zjO+|T`%7Ky&LfPRJhDK%H_Y%rR#e^eEDH!(7E`8!6;p*NDMAwtM%Ohl%*EnKAr=b9 zIEm*Iyb$7rL?{-ICc}JFD8`qt5>kf@XyOu9`-2Ua#d=~eED~9Qwtf(5NgEkpp=ePr zpG&YT-y&8kpUaN2Se7KidZYbo)Fl(qk9S-WF1aj8ADTq(HWBvjt5qk+qqvunq8ayn zZf<_Q^>JIp(;ok7(LXgcwdTrX?b!JA*V}D#;~V6G7w4n1H@ZK%{orjpH@+g9td0_U?It4UO z>}?+%iSA>4-)Ev{Dz6`V(6a9GrJCnv#U~^seNq1$s9XDJ^Ut#%ibqa2^gVsJB`}t( w<-SmNZmoLff9*Rw4Vx1;XY()fZ+w)mlDaV06z>c?YxACWD$ylfi}fA-1s(`wsQ>@~ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flock_actionbar_icon.png b/flock/src/main/res/drawable-mdpi/flock_actionbar_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..04da3ea09034fd8f6098acf053e40def097864ab GIT binary patch literal 1825 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 JPDHLkV1h^E;You`&iEP;)-?0 zzcyQXGq5;-(Snm^Z%l7yZQESRyUoLl;n@~(kG2DwHb3|gwcYmt%Mynd)w_CqX3Vm_ ppTc~i(Nwqn(yB-2lXrTQu;;$Fkqcr|`v~+ngQu&X%Q~loCIF^tbrk>r literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_btn_check_off_disabled_holo_light.png b/flock/src/main/res/drawable-mdpi/flocktheme_btn_check_off_disabled_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..db190430ce0a5e6daeec894453fcddc6e8fa5adc GIT binary patch literal 329 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UOiAAEE)4(M`_JqL@;D1TB8!2v z2N=7Z%(eqE(mY)pLp+WrCrGd!W-wS()GH}@a>QWZRN c6Vp?JQWH}u3s0un02MKKy85}Sb4q9e0K-jeO#lD@ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_btn_check_off_focused_holo_light.png b/flock/src/main/res/drawable-mdpi/flocktheme_btn_check_off_focused_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..b60286bb7c449bda8ff28498162812df90bd7615 GIT binary patch literal 317 zcmV-D0mA-?P)i3000XVz_ASGBe1Cf^np&K`M=h{ z6!^IpAob?k!>ML}ovM^VqMU8k&)WA&?*j1gYk_rcn5YrE%bHLE1+W4-zCnzv?^Tp7h5(Mhj3*DJWs5H#%SEC;9;!@j&zFtN P00000NkvXXu0mjfdgF!` literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_btn_check_off_holo_light.png b/flock/src/main/res/drawable-mdpi/flocktheme_btn_check_off_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..f5eaf804b1b148b9d55a120a8d4cdaaabd4943e8 GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJI!_nJkcif|(>C%p81S(4cW`DH zw}~%KX0sCeEK$-SsKj;2Nu_zg&urm(zdSpImLD_Uy}NKm$bdm}MtsBFyQi>t}P8Y~1@IE8dw&XhH*vib7+Uw3x#6ux{VXG!78HH3iO|b0nKn2hA#j| z00Dqu9~?jeFg9y=yHoHTTQiS_<>jRahW*O^7gjQSG5C0aX6w-1Ev0mjlqE{iLkL$h z^DKds(qW$GZ<14vfTRwepNI00S$00000NkvXXu0mjf_5pNL literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_btn_check_on_disabled_focused_holo_light.png b/flock/src/main/res/drawable-mdpi/flocktheme_btn_check_on_disabled_focused_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..4066d99460d0f428f18046184ff7bd81ce4d8e32 GIT binary patch literal 579 zcmV-J0=)f+P)BjE6vuyeRh&2kQ#y1gp@B{%p-ZRY`Z5GchA#ac`2zk3`6gXM(9kShjBFJR{GCUu0OM5G_VD9fM~&3x-e2; z03@oCn_7Q=}b?i z>a{VwXTY_!)(P^c1d{%jz|no8&kxVuU&xL^Gz4yd9?%7@f!GDHNPS1v+KUi(V@y;q zUk6?RZ-KAnXY1-4EQk3NxB`B zj~AU*vdc$qRiDdpye2!`2YT|m(j168t>gwc09vk?bs3lg7RzJqtuW64yb!^S zs04Ah%zF3---1vrl7UTQ%x=T@o5298PnsRiYSxa@jkCjVRpxmHy)|w}=|rOTEnl~~Rjkq_VwIAL~ySn8lM>wrUv`AE)NaZz$bYb@2T?gJ~W^alZK!0`TK RJ-Yw^002ovPDHLkV1oag1K$7u literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_btn_check_on_disabled_holo_light.png b/flock/src/main/res/drawable-mdpi/flocktheme_btn_check_on_disabled_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..20e2aab12b39cd77efd3a0ab36420a9e123cd181 GIT binary patch literal 508 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UOiAAEE)4(M`_JqL@;D1TB8!2v z2N=7Z%(i1-V3hQ9aSZV|{&vbnzrzM1uKuqgF33M%me3TAo@?_@Qqv=c$3H2#d8vuV zU8`lHD@0b#SeU!$(v99DO#$zxSiG09zwP;8M*W18?_olIb$wSOUoMZ(n~@Gelm-xpUX%JwH0>@rF6Ys^4E-U%Yo+wy1a8;YXU4cOK1UO)5-@uu0~c zYwF*`WSI2o^%TWQX*rP&*Aqwb3fi|k{ylrg+`xhgp~?fsIR$$pVr4}p6tyWTE*qvD{bC>mF=5@HkX$ToA6=fZh6cJu79oa)R>tO5CMLQD=2iv<>rHpFqiD#@ zPsvQHMAu+uWo!h|5V1dG8Bl{H$cEtjw370~qErUQl>DSr1<%~X^wgl##FWaylc_d9 OMGT&!Dh2~S<_@QosQGVj1O~WbKLB$Lh&UZ9A=m~ z=l^}*|NPGdmheBrbE5*=?I?@Lc46I60TQVq8q(8}T3U=ZPoN+W%GDTmnjgI^bu?Nj?ECu^$NF zI1B-|y{A4O4gS*q(ayRH7o;)isaPaHq~CRwYUm2T1DqjiCTy@36p!Wg`%wJL=Dh*0 z?OPMn2U}{!V?!lP&%9OrS>)@R@2~9`vEP2Z{^CzUTsl@U^LwU$>%dbPj&r#W0wK-7 zcSwEu)z;O05w7Yy5$JvJi>iCH_kUa&ed+F5orNOJIR!F7uI3u>vXJ`F_+P$IkG<{1 zXjNB)t11A;6Xk;+o>n`%n`aF%F835HCiloha&v|F1bJX;7T*xz9c{HIBIpiz-&9%b zT(V?eck@IXC;g^OQcMQiety7r`nOWoT|iG8A&phVWB04;;~)FKEOY;ym9vlYs;IOK@g|}DrR*8T}I=M*4q9S42QgX zvV3qfS#sdM)pH48qaZc}xs`V#%8~)!_wnVSvn5;iyj=NB3zo^j-^u*nZ2Y?RXUU+ad3nEQ@sxR~`8F@v#IjqZ*11sKT|DNq#*UumJPphei|Ur~Ui; z602eW%}@2d$(=P zTW9|a>=({esTsz+*A>6Wc`*Wn2n!L!kiMI*)K6+JoZPV2Fm3bGN5(I)kZSj7;7O1D z&jJuTp`1$hT|4x^n0r#XPUWR$6?5MyrU>22b>~sXajD`Q)hY3=iGOCsn7kGIGitd)<||cYk^h@Ur1a7Su0#11l$h1>)FMnTmdpM1DvC{ zD_J2}BGyc{pSc96z*j(Do<7CGWT8$LpU=A5dZ)H literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_btn_check_on_holo_light.png b/flock/src/main/res/drawable-mdpi/flocktheme_btn_check_on_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..6bfb1d579def387eb3a8854adbcd8199531f4179 GIT binary patch literal 999 zcmV(;)G!JK4meG2agJ_MT07R}0&RK(fFZz}_hV36yY$G&0H4WL$5P05=zRBCRigEolVs78T&p zy8vi>Q~{9T#~>ff0s}xLw*WAu0W2#|uLEo!N&{*DJOJ_mcmWh0(|0ufd~b0Df3FH+ zVt!5qd_Lbp0C(g`5~wFc+f{fuaCzeM_UK=K zANaBI!JhyO044ycG=Mp&fjc*$^@kDJI8>C02V^D0~b2{0kIEO|ly|7yXh#FqNnIn4p($s+*3wRlm-$(wp; z_wsSdmri$s0|gZE-SaMpse;JhIMhz~cGi})2Vm*|fVRQH$h~3r!7H1iF{Fyml2^|~ zKqw@tTm54w-#c5gNkze9`Mnk$QMKQ#4clwWu2;iMh{z*Hx=MmgRlP$<3g))DazZA8xAZjRVj*fk`ds zhyXYBW^AUn%U!i^&qqzwU>1)4yNPVpnlBOsuJaTAjX#vP1VD{6_=&!fV9m1s2!Jt8 zz>o-JNu1Me56|^{Y(*9!X#%!|y0WX)VQb}A7ZPjz-!BXYV5PuYV?~kZs5|)U7rpVb z4;I#Oa2=UVh{%1^y}uFaXw>HNElsBU)oyyf^{?NSY9lw+s|U?vHu|_y~Z6VG)W< z?z6z15MYccqIFCFM$IR&c&NKH*cvPD5FcHQ6+}YaCBfRt!8kHG*QsTTGhj+^z)1>H zn>FQR4FKelzlR;|nSXFbp1s#*N!Inq)5s&&C8sp$npE+xOh9P>^Z6LK0#y3T`mS=o z`1bme{nWCpbL;y5K_=IkoGWa~ONe6voCbjOamu7;wbG{Nnl*X8;bM8VZ^)5|Q8!fO}txk+F zFBxNbq`;S|sybf2fQ5XDbS9r95F=kp6 z#k?%btA`TS+KILHHWnTw&{XKXzjn@LWm#Sl5v;WnV@yUw&o<%9<#JNjwcLw<_x=$P zJqeHJ&bh28it7Mxt=u{Hys9d_7XhWzy$~V;@I4GotE!p@INyZty6(;bj4^1f-wPpb zA4oJ!OfPtpK!-$cc(g!^#OFVO1KY3SP6=!6S1D!Q7v5W-kr)n#-}-(ZCD3;B?JL^^ zJQ1BP7K@ucixbf))-8BH0V(BzF?JW*kThY8c_GB2w*X_zODTW!f$y55_k{liIJTV9 szZ}o8{!2YpEC-MS_=&^rgb&R87dhGwL2KQo#XYACm2Vkzd&;f9WN>fu&e^QqGd@qg?Ifi}Gsr|MTwDN|Y zA1GF*4qSltL+`uvT$i1z2z8(;00U?zubJx1qT5bbml`9s(?+nz3tAs(G?r~C3XIqG)(R zZOJnT3{v1JG_lZ6G}irlX?~)_0smX}-SJ#ntR|eickL#u*}7_#U*$ackiWNlToTg4 zr#+Qyj`ZaHKV@cr^RZYi}a_TZ3+4UUY*wW~D{^tZbnyZV!Sk=BM1)_V(4@o27Fl5C6)ZyldUg%vnmFLhH}Z>zGj7$9Up`*|d{< z2kH*lsk0Wa7D(%~?LGhJcxv0PMfgTe~ HDWM4fQ|x?C literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_btn_default_focused_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_btn_default_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..b7d42d0e2fe8dea6e9b56bc6de9f7fcbdef8986c GIT binary patch literal 283 zcmV+$0p$LPP)b%fESKboM0|2r7i zo_u6DEyK_7hdj$5;K@e@78!m9Q^pF0U&!JtSk$sGB7%A*nf9ea&0t|*K(%n#Cz{cG zIBMajh1e~mYehOBKEzsM(9%M>f>8@cEhH6?+ejiGU~AJLgLHaW`0v0yhF1&>3`+mV zEcBQlMajKrLxz9oa@gCY9Sm9w3=A3!&mP|T%w2WVt;W*8Y5FvFIS ha18=ek5?Twa{)cHFu0|RqRRjP002ovPDHLkV1l-7bj$z% literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_btn_default_normal_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_btn_default_normal_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..6ae931d9b7b5a1090f261e70c4aa873c74bdf99e GIT binary patch literal 247 zcmVj_h8tTw48yFO!xf%$H3q{@7ABXg|a5Nu|S~zMUb_?lRkq(Ftu@>O8w2-b~)WT5< zNd@G#lE?>xqnXISz~HnhiedG?|KvG{iIL%7*fNGt1_p+G#9GMkzk{KO9LwMaS1|Np xm`U#r(8%b_FfuS;hAkuE8U&^uuR3hz0su8=C^E0xh4TOa002ovPDHLkV1mH)Z1?~G literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_btn_default_pressed_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_btn_default_pressed_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..fe57d06f21ca9b84534c27dd5eeb602fa94d95e5 GIT binary patch literal 245 zcmV}J6x zy|!vgL0xQU{ecqdRMkG4$rds0M?DOe0}5Lp6o#L)VVq9KFcy(2UDQ^Kbf+k^LzBJEN)!=X=+3o>5PyLY|ASIQvAWL2mF7yR zZNaUItuCYvrN}U*Gj&KC7xxZrW9B2Vy0Pyj5Y9d4$<6tKef{s!(i{pMvcM6*P_L@M zs%EqC*8oC?9$*5nfa5?<#AFLt0z6<|v)S4+fY9L-a2*%`-T{liGEh+<1~3Ag1x^Fs zfk&Fn$3F%L9WDTufCb>W<(mGXOkZjhfDcr>k}!ZP3gm@m^D;Gn3VsoI zV!7sxn9fcuGt(GG~_IG(Xw^A&Iml~{Bh$7g_>z)Y%f9D&%ikf>k=+a=mifh%4~ zWRSmZvQzM`02IVHawRJOZ;`$IEAkdl?}w0;GYPB#)0)luPQH^arA6eV9E~yALYA)w q%CR?+&r(VnVwXa`c%k!B~c)Rle0000 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_btn_radio_off_disabled_holo_light.png b/flock/src/main/res/drawable-mdpi/flocktheme_btn_radio_off_disabled_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..a67375ec8a3320b4513ade5da26fa804d2265284 GIT binary patch literal 479 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 Rk(2-c002ovPDHLkV1g8LjU@m8 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_btn_radio_on_pressed_holo_light.png b/flock/src/main/res/drawable-mdpi/flocktheme_btn_radio_on_pressed_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..7d47196e4440b8f35646fa07b72e4f9c06ba174d GIT binary patch literal 1118 zcmV-k1flzhP)`cAMzXW6GBct<(Gtz(-3ksI{9Qw z3?|7KDRPWaBq73jwX^flGu1gvkG;~)j#l>i!5(_Iw%@Dj>Z)pi1rY>+4WOexHUJX< z>7N-u0U!r329Snfm@l%gSb*3CusTJ25o$3L0HZJr)3*{31c3>lugT2?h?`IV7y%fD zVOTU77<0&70M|4*HQ6i(0^k8~eBaNbD4G#5BOnNZ4uI84-0_j`+j;}#2EqF z^XpjbtS<{gi&QMgHx44C;tBHA|$;{_270f3qNWm(>2<}D)H z^E~gNX_{@UB{Rn?&@og})$F1)&j^Wj$l&Eh%LO09@BSv@Gks z5aKD$MuLS9Pc6&3@4D_G07xl&N~sTQ+um&vkeaAQr(V;qj^hmTJl{{#bYPn110e+W zdcEJ%H2rQ~Ap)vun5OA~i1u2V69bskrFa8#uH*YI#YPlGrSJO|=F9;^B#G!np66dm zDX$XIaa|S9LrU3;JRYJt zhSsUbah##!IFA54T7akt6U_x+Y9@@Lh<)GBQH{}!_)KSmN{ie+}UTs(4Q5G5`Po07*qoM6N<$f^8cJ00000 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_off_disabled_focused_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_off_disabled_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..0fb507dbf21763548ab7b96b1a084c6c99a455d4 GIT binary patch literal 329 zcmV-P0k-~$P)Y z(0PDCX8lzL2L=WPhA=&beMPnm&q&cvvJZd%L+JVamvsGve5imfV5`6&CdA3`iJ6Jv zpN1%d6cJ_+W#JbF1_mB%681_Auk4i=UJ+sT7i@yqN*Sh5T?RQxJ_Z3MGUAwlfq~&4 z1H+%^pBO%@zRd6l!i9kU3=IEq6>Cfkl&9c-$d{XE)7O>#E;pB$IQNIg-+lmvZg{#lhIkx*JKd1$umO+jejhgB z7dPgqd4Id7`b0%7{UzhBFJe2J6rB}Cr+CC#v8wBF=4NI!iYI=V@F&;%z@C@IYrobX z7YX>B`Qlrn$R|_Z;xnIX*6(`rr<0@h{_j<*{FDVV#cs?u{CqH~I#P!()S#GAfw`c6 zT8K-SceR#VP$rYwgZWIQpCyBYe3|n2U(C^-Tl~~NMIvjF0ngz#ch|iBCBtnas?0KF zTW)^rbmH`cDXC(6*D-fePbPx>~A>(d_Dcet4}tlYsb X(G=kA6q+jt^dp0(tDnm{r-UW|7)O86 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_off_focused_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_off_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..699b153f91233eafc7027e4a59b3868afb2c245c GIT binary patch literal 366 zcmV-!0g?WRP)YU6vy%Jjh9p_2I?yQ8FX<~1aWb4^fmel-AcE#Z4J2& zy^%CM#HO(cXnv3(Cztcdg}}Li^cZ{Pt(Ox6#wi;8`=FAFrb`F7WbqdYfos*Gooqb= znl3JIlLL#RXR5_G-d+m}bqu>&5Np~V8m7R1@X=IY1y-O47CJCX*`YWwmI)S`3jFm* z12`?t^BbQ3!8*X^8t=Oe`b?(#Qb6us+3vk?3~;o?tp(>!Rxlg|xc-1Wr3eR~+>v2- z^MK97y>oFu{d8x7Y}7ahPO_j97>)uS4IB#39KFK0QOA!8!vHOT3La`&tk1*+_5qK4 z=h8m#MC{aNq;si_2dw4{XE)7O>#E;pB$xJ|1wD9vk89kB9nu1DV%Y83@vB^-9?!A*98v54q`PP1+wO{!d zm4w&-FHoM>4_Q53%!{Q82*c7gs-2_vW)YnF30`Ia`yRMV3qnlsR7Y zDsbL&@$f{=R1@)xu0<~eWtps ld*SvYf14xR`4d*XW6tRC*d{w`g9gyU44$rjF6*2UngF}1iqZf8 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_off_pressed_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_off_pressed_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..98a9f10ac6a5129462dedf03fb6263f76395ee13 GIT binary patch literal 298 zcmV+_0oDGAP)QQp&xQa_=*iI@9!u zF=l+mIe|(k9z~JUG)>b}pRrPkoIMI?%Cfx0alGZ+?-v_mP*v3hK&xoiF0%%(@uGcn w2k>YHZ0L*5JT5Z~XwNO{?;!8|&OFNZ387&|jw>r>bpQYW07*qoM6N<$f{J&2c>n+a literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_on_disabled_focused_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_on_disabled_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..8e6259b5d29c28863366e80da5f8d4506d612568 GIT binary patch literal 299 zcmV+`0o4A9P)~7qWA00^VP8!BQ=v)_4Zr}REZ3I?mY_pTSX}m_ z|A)o{JbkdU5lT(>)ppOaFc%C0qm x`~FG_E1=qbS#|Py3UYG6XzL)kztK;Z_XSPKGWbE3r#=7x002ovPDHLkV1kP1fL{Or literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_on_disabled_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_on_disabled_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..4fe5e42216d78167d69ddf5e82b116d71b430b93 GIT binary patch literal 353 zcmV-n0iOPeP){uS$Gu_D_ct&D+{hR79?nc8v{EwVvt+7z(t4` zCZS+sw_fP)y>yZ4x3N`j8M@uz0->I;ws?LN`os{pZ@L5pSs#WLV zr>lkwXwx(`d7f{y-W9MV0;2jgf^F1}vDT7hnE`etuR*=r4sb_04LQ+mouvE$6-tQpfkkx$1ra!=GV>%Zj)y00000NkvXXu0mjf$pfC# literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_on_focused_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_on_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..2d89acfce2350913bc0ad1a3bc046a7d4ba74d55 GIT binary patch literal 313 zcmV-90mlA`P)o+`$!k5NGhttF5-8{UHz@ zFYil2$e%#Ev}r}-)kuSGlnwoTppt@m&I}lme?k$kRAZ*`(g~oR;{vM!FdI5hV~%!t zM@V!m*diy)H#Ho%1OCCGsev`H28zH^4YQIJijMI~V5zEsZ~OA3Oflbnon%eX)f@E#R= zLJ_b7Hdu#BOYc%64`>wx?hW)NUG~qalHZfMl4HRv2f5~_=1FzWf_*o_bcN{q00000 LNkvXXu0mjf%C&%j literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_on_normal_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_on_normal_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..c5f5db22b0b6b0930aa0586f306f928c4f5f5e1d GIT binary patch literal 399 zcmV;A0dW3_P)?%2Q@uDU^ti=lN}~*SnaR zGOoq%4u`|GlrmU}Hc^&k`v6=3q z^L!~FJAz&F(P;8Z7XV9>-j~^3Ay6shdY+ff*2!p!MSJw@9$y1MEZQS7nru*|lr`H{ z+6PiswK7zP057E<(;*#WRyFt@DU0Hogw trs|8%|G3OJ!2H=dKX;HX|3aTF_Y*Tkcn4;4gJb{z002ovPDHLkV1kEZuZ{o! literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_on_pressed_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_btn_toggle_on_pressed_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..54f56e187b60887bf541859ef830004f949fe711 GIT binary patch literal 361 zcmV-v0ha!WP)5RjLb@0S8Pz#*2VVD)?HH+1>!hv-`lm$R4IkBEMEb1 zHr@A^x#}|;p?3fu+X0&TqVqp4GX~H-TjTEz(&i85>3qKc9VKZ36xv4800000NkvXX Hu0mjfB1E8j literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_fastscroll_thumb_default_holo.png b/flock/src/main/res/drawable-mdpi/flocktheme_fastscroll_thumb_default_holo.png new file mode 100755 index 0000000000000000000000000000000000000000..ae36f794fb7a16350b45a442416b86c753144f7f GIT binary patch literal 168 zcmeAS@N?(olHy`uVBq!ia0vp^azJdx!3HERoY(vbr1Cvo977^n-(KIy*Py`D@^H2P zd+&^zi%r~<3o}(SJN~Qc@cdOU7g+ON>txtrCT3>l|EzW@JD1o^>)#uo^U-6+J^q}D ziZiACSEjG*Yeazkhu3oLldG6q;?|yGmm9n(QKb!H7Y!HvE@|HtSkfkTagnpf7A`^Y;O;3IO*7Lq!uI5wf4cYQSfIFHLG)$M zLwOOcqW|1?gm1QEOiDSSZ!HoBNqojak*0BRD7EG=APA|iLU(gaXn0`jPQCKl5 zm7}M|b<*0twI8`6KE!NcEIV-QRiRz}N!i=_moB~HKHa*b%C0H8^AMNznhkKGz`7u> zce_p9l~pr7&076jxA8%WKI5``tFF(4^P+1oO?3%=d1IciLLeA`JiP^7;%>=}P6krkJUk3Z3M`v#;#9PNni)J@{an^LB{Ts5 Dc8?rh literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_list_activated_holo.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_list_activated_holo.9.png new file mode 100755 index 0000000000000000000000000000000000000000..03b06ba5c4aabe2720b6e47436adb4c901c2c5d5 GIT binary patch literal 110 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqrk*a2Are!Q6C7CoFO=Yukd%;+ z_*ZT)C-nf6gtkP_j18U&%xWdd8#NU&n3P@B4wO7DVpnFc31jCnlz3|oG=Ra=)z4*} HQ$iB}B2F68 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_list_focused_holo.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_list_focused_holo.9.png new file mode 100755 index 0000000000000000000000000000000000000000..8f73437f2f75155bdc6a539b0da9152442322382 GIT binary patch literal 117 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqHl8kyAre!Q6C7CoFO=Yukd%;+ z_*ZUlCglK=gto+`85=wmnAJ>_w>uejF*T_fD1F?!a7Kl2!mj_1C$Te_-Q?u@aGoU! PXbyv?tDnm{r-UW|Xp|vG literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_list_longpressed_holo.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_list_longpressed_holo.9.png new file mode 100755 index 0000000000000000000000000000000000000000..ba6538fbede12e0b3f407113d33c19bb0a6b133d GIT binary patch literal 110 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqrk*a2Are!Q6C7CoFO=Yukd%;+ z`1AK~t;B|gUTO2Zv_oo*JTorz9$D6~fU`$rCd1|4)3QAbFVAss9sTluEzkf4Pgg&e IbxsLQ02+BB`v3p{ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_list_pressed_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_list_pressed_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..886a6ee0cddae3d699011c960f2d74d5e71428a0 GIT binary patch literal 110 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqrk*a2Are!Q6C7CoFO=Yukd%;+ zNIN$t^GE~hlI6?iryf#k@oJyIHz9Y|%SiynA|kd)58s-{92FmHki*;@KxND!%;H%=F4ZU(n;yg$GVRgBd(s L{an^LB{Ts54)-;@ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_progress_bg_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_progress_bg_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..780b4b2560eff39992aad632c73845afe508b358 GIT binary patch literal 161 zcmeAS@N?(olHy`uVBq!ia0vp^Y(Ol;0U|59*B=E^EX7WqAsj$Z!;#Vf4nJ zaCd?*qxs3xYk`8!o-U3d5>u0Z{Qqyy%*=ea-sH@IhBFRK|7ULCc*MG+bb{VNrlq}w zo*ol(t{BH~U0^i*d+mDm&KI zZCOo)2hNCv7LsBmS*%wa_WnTo#y4UWBPdPIaquu`$D{Q@>jA(OIJv-ObBdG++L|0$ zfalxfy9>OUW7qPoUOpj z%zU_BO{b}XF;8}h_XYzX`13x&z)37+O@kMZ<1sN!1gOL~hU)^O>EDA#8f;4nUnjKe cWQvtxNM+*+`2K+LDbQdBPgg&ebxsLQ07CUBasU7T literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_progressbar_indeterminate_holo1.png b/flock/src/main/res/drawable-mdpi/flocktheme_progressbar_indeterminate_holo1.png new file mode 100755 index 0000000000000000000000000000000000000000..792f025a6599ef31500fe314adb6b43bff5813dd GIT binary patch literal 409 zcmV;K0cQS*P)HKw)o79(wGU*wu({bkNy(>%ePxo(!Brj(!F4Yv` z@pPOdw@IE;XCBI=XS7bonXC7%EX_aWG%d z?^~Zv0RW0IBzaApuL!4Cew%!5a@(ZdJd{b#Xq}ETSMOa}ntx2Py*NDOKJ{&i>wS|S z>(Db=r{m1kdslvSbNyTD{0#KF&pNj`K9m~sP$oU2bvn*my?144{*S+t*!|TC|H1zJ zC9s&D(K;PxuHL)y1vuD%MGCk`%F6N0=-E*!aXd~!$9ucG_^8zYih<`wLS^Ez|# zH<#xm72gM6Zsa>mQdudc7dJ`8Q@HRWkeubgJZEG40VTb}+-Iv3Pe>bXJ~iPR-@b3> z-esPfv)1eCwa&wHcE7t3aZfBm`oW~XpRU#vKZ^M(p2EDR^uB*%af0KSGg{8;stros z&zaHeQChG~D>PdDS8BF*-LdDNzU|+f+0cH5Gw_RJe&h0YH&_e&?|_wuACN8c|NJ~B zbk#P$FO~TX&p|d!%lo)%-XmGLo0A?M(pjOr`P2rMIKH^swyuxB>U0{fzx(R{>D{8- z1weNn&1!H@s^j4}?B1B&Hv9g)j=7Pk7H{rtv)*>>ohDoQ>Y%NqkIXBh6N*3Wd(>57 zzVF}<-gAP+t9kCltUm$qm;L>i|6X_OnReFa-`%i3A`mFK^SI{?Ti1tR>z>G|8&3QF zxlri#8Ghy4z4Mj+c9;DT$=n9fEdR83Tcg(`717g2y(g(;DnUZ@Cxht9UEh4emgfMY Oj=|H_&t;ucLK6Tx!|Qhd literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_progressbar_indeterminate_holo3.png b/flock/src/main/res/drawable-mdpi/flocktheme_progressbar_indeterminate_holo3.png new file mode 100755 index 0000000000000000000000000000000000000000..03de2ead47fb0eca51c953de8d0aab8cd10bc996 GIT binary patch literal 533 zcmV+w0_y#VP)4235=2CbE6?OLT)FTfUci+L@fIG*g@}lhASJr!j7j5E7nV4G`#unenGmKk zKf^~7bMSGEBuSDcF#GQgNRlM&3xk&=Nzz0NUXmn96KQ}$`T*9YZ8>P44L1E3u4)}&)gplyKr(ZLJ)J25A~9smEsc1fP3 zE8sC5I|FWToE1l@j$JQQJL@){JZG1{Q#w}LoaH`_^V2Q;4V8Re+W_aI3mmgbZFzs( zuI-ou&x3u(SrN_Y7em(zMPc2>GY4MMeywv>%1_PcoMjD3QIaHSFEqdd>yH{2^e=}k z2JMEl?Z~$zUs*j9Cha@Uiey;Ft`~~Jx{W8ooaH{2^3%6RjRzmsoZ-v$Q}N|`?SkLx z*!4m+vTozq*_Z2I*!($(HeJjxusx;ENjT1mru1+5*0E}5-NsY;R6}iZR_fz|RUh9d zd`{whe1(DK4{h3>?)Ouq>bYJhBI`Du2y@o4Az#&y6eUTL_JYAnk|b#&1}{mHq=|e2 XX^7C`S|!&B00000NkvXXu0mjfdl>sS literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_progressbar_indeterminate_holo4.png b/flock/src/main/res/drawable-mdpi/flocktheme_progressbar_indeterminate_holo4.png new file mode 100755 index 0000000000000000000000000000000000000000..a023ecb516ce6e34dc63394a82544f3e772cc158 GIT binary patch literal 517 zcmV+g0{Z=lP)JzB}fq|u6ib~;mU;<=>=T55O3j;T!<8rQlz9Vn%Ed((@L1W(eDR=G-l$! zdyIdYn9*jABuSDI7@v9qk|at0g~3abBqEf>JU0b@0sx(>I% zb9HP6++iJ*pRRs(T~S2#YCLsKZh)8S*r{vsQ&zgC6}+yFxac|CBv!6%X*)5!|D~ilsjb{ohs{JT6>B>q)^?7Ydmn2C_qyU~c{8rY{zof69T~`!^y&6x%TyoUI z^$ULYI*C>F3;biBPS@)stb=mZ^)LGNvm&xrbb*y#&DN3O1 zKSj!h>xyz=uf`LxCMB($WlEPMNlL`vB}tN$h`~#eBq@JzrAR4KT=h&|!<7p!(hIm2Z{d+#h!l}hB&05q&{$H7CV82*-wz5Qw3A`x z@%qQcjJ8W8Ns<&`JoE%4Ns=yw!Ap`PDTu*Kk|Zg}1h}FIVE+wadF_?e#vRjcT=gg4 zd%@5Ba?fJfHklw&rg9_i*ePTst7hT z@7WZafKFFaeUGsxX;{`}tQ4|I(;PC&3qQA~O+ zrgKoQZ+ntbVO_>bVZHbBQ4TQLE?MBs^_Rq(>vhhK)wexKL$EGmrI1a2y}5ox_m7j< zlo#_0JWWlHlW@;;E}H&r-}=_jtjky_WRruBllVIHi59ZsZ&70M<8P5pmOV)+vMysK svEC+ZNs^=>1}{mHq#y<_Ns^=>-zf;mDKr8DGynhq07*qoM6N<$g6&G_+5i9m literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_progressbar_indeterminate_holo6.png b/flock/src/main/res/drawable-mdpi/flocktheme_progressbar_indeterminate_holo6.png new file mode 100755 index 0000000000000000000000000000000000000000..9d812e36b404336c2c9f8297e3b14f726c5e8a4d GIT binary patch literal 533 zcmV+w0_y#VP)|3Y5fLe+l;|QEnzqq`%)HR}e-H>E z^o4nk$zzgc^fg10BuN3reP4hiNz#8|<0VOw6vW0$k|Zg}1URB@z)Cb>Rcl+dVvZ>n zQ}L>;4K{OZo^|N9YrMj_OtGxNyxP4<#qK>T=Xh@PLKEPOy7@{8%o%6}EP*+&XpSEP z(?T&7FBQ{*?mQpkq@csrRpVov%M`~N%q#ULX*`$B@e=o~onB}HJW?)UDd08hm5>wQ zpZ4dzu*kK--yNj-j-z#ZilqONIHysk3-_)n(P zonG3Hh^cs|z*}qIJZor9f!Ai=xlDbq2J?BQUfr5gH(TeF9nUH7X@@LHk`!bDJW>8p ziV2S zsqx(ToW%0?^Rrv+KScvj?LS32XwGHon>CnEd*PrcJ7h_cq#!n4k|aq%Y`i2%l7f5# Xl`Y2;A<$m%00000NkvXXu0mjf{xkhc literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_progressbar_indeterminate_holo7.png b/flock/src/main/res/drawable-mdpi/flocktheme_progressbar_indeterminate_holo7.png new file mode 100755 index 0000000000000000000000000000000000000000..49bbeb475befef1064b9c3f144289106ebbf2989 GIT binary patch literal 486 zcmV@P)Io2}PtxSDvZY=&Fle#0zv?dW#;Zi&CTu5s|cuBpM|UrkPAa{C*&W5QgXQ z=lRf>+3yBPk|ZTCAG!cZlB8SV;3Y|tl!$|uBuP>t8E{82fTbG3^4jZIt7A^uIy6rC z)eA1OVLmh^*LlyeA1WU66{gSm9-2ioHoT_~MQ2ZWFd2Bv3MfJSf4;7pF z3e)F)oo1ie?}jzrT(5EP=6ca3)Qlf+8+1dn~+L&IF^FbkmG~7P^ zzFgAROg7Al+9N<(Yd7$w+2=~UhSU(SH!ASv869d zPIH!~jCV58@HFN90_RS(V}9zo>_^hM@(g%B;*EVt1>Ui}OK}Z65wBLoma_B!yjJ_> zz$0c!!_$;^=T5a{e(KVg+yigbzQ2x1LDhKOc3EM^t|+_Av>mxt#Wn-pcec%vhHS?6 zBH`SrW0{}28L+IjPZ^WV7`szR%{out%zBxVoO>2p!}VlYTGPn zh<5Yt+^J)kpSmu`qyX~C=Yl!07R|Z1UR`l4N$QD>mn2D2Prd*Ib-4;@?JsKp0000+$P0`yX)>KD zBKSk-w3EF5oA)wF3jg|L?QAm07}Ek=TWMQ}$ZBf$qP zV+udMZ&rCTsVhGNZfDEUW2x^TbIj6@?*lBbYj|=VXw4rojh*J@`h!;jy*aHIw2FRA z)X;<*VN=z#=D;`yU`l|r0gXgto&?Y`ykO@;QNM zz-vtiXFiH`NFB)v;1qi}$y%aInx)K>040zP_JGpE?icIy3A|&EVZpRl22h9sI8cZ# zHY2Wt?pQtn#v(Gyjl1a|6St{2fR(?({p-)@3#)5{MD`vVs{jB107*qoM6N<$f(u#M ADgXcg literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_scrubber_control_focused_holo.png b/flock/src/main/res/drawable-mdpi/flocktheme_scrubber_control_focused_holo.png new file mode 100755 index 0000000000000000000000000000000000000000..65b177e92597088ad6589e78807c7c4101dc4fde GIT binary patch literal 619 zcmV-x0+juUP) z$wsdTi!=nk0^Vi@j^TlK8spv)VWsmmvhuOPyZq-W#9IzAyaqm-7`z)}37pcC@mPRE zR=@0Xo&c|$C-FcW#34#&1B46U15zU(dwc_|5_8_D?bSjoz$2LQQxCg!h zcgDOzxx>+foKDK@@IIRBRM0h&K?`%DVvI>V>An95TqedZ)yINQ2?!2D=}(P^ zOjs!5`G>w_IsON6#?Hxm8!52Vc!OPpAdl1D{H}n8_Bpli1F3ckKh?)Vc(Qv11cxd? zumZmEZzj6OuuRNX@yEQqL(BA6s}>6gmR&c8?rZq!3CF1`U=*hIeN#>d@E?2~{{?zulI*-0XK literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_scrubber_control_normal_holo.png b/flock/src/main/res/drawable-mdpi/flocktheme_scrubber_control_normal_holo.png new file mode 100755 index 0000000000000000000000000000000000000000..5bf08688b93576eff82ec2f1978c2f52b634b836 GIT binary patch literal 749 zcmV_Cue4!XWoyQch3C#olVfo*2FAZC-gh^9bg-0^EpV;F^+MR z&k3fdC15KFFX2O6#@k6=tT0Uxe`6QFV-KTsLRJarVTku|9UtZUG7Rxf_B+Bac!)oR zRa)J%Oh7*;yN<7LZUu3h@N>9<_i+zD<3*c@HUWK{$8B5_F4zRUoabBN!Us596tNh| zJ+boJ_~L(%H?fUx@U^ha9Ot5dp)mV3-7;hy2Dph&^LML&Eqow+ei|!Z#1OY|QDbpU z0cXYPy98WUyo=9;Tjm7xMSfp>jpT*zGwc-ZVv2z<|EfH1Vp|B_$@hJc@1u&bZ{bSz z9~S|`{Ido-__9F`AE$^P^SKJe+5?Qsp)$mq739Mf@~qGzAP+jYwQtLf#Z{ zxniC-gd2M$2)DetCoCg4jXwS4tRj7_C<4aniiv}YKI?*zIHD&-z)T#M#}xzrSVM4- zeb(Wh3r6@YTUY!g#7>#7>GtsJ1`~F{6pw|)W=#P^96VGP#hc=ZD9=s;zA0cTOmk0N zMs&p??u!MUr2JyQ9pfkbAoKK;Fv49CGvnrm#jJw2+tSeS0BGn#?fZMlV_8I|2#Ie|* zo(U6P$+jbo)G~gGm)UPu1oMFvH=-H=1yhltBT+^@5^=F@ej_XA$vDSU-ITl3{^b@t f7Vc@go_EH7TacOTN+i?*l`{pm<4;;W-co&DOkXcirKn<7Ue%y%leE}EI zP5cm_A4 zgO9NfzbTish6^x@&1w7|Jc1)t2sDLsvuVPs__jjeLDX@LNL?eX^UxrPK@+YMJD(MK zd>OwE88}A=@8M6}iQ7`}UBW{k8zA$bko^ee&LDU{z7rd1;od}K8T|xuw|9hGzlXX)9_G^PmiVv}1?%hH6&MBsSIuy|fhQVto_$@-^C-8RvfU_RGe$|AoxR6YV!(s;iibJZb zO%GxMH{ho9|4BS86h0*Z_*=Mp99IcXY!Kb@FZ@#>@MheOtHlBLSPH&0IJoG;WDK|B z;Y6YBG-B8w(tdB z6mCCSw0IQ)Io1hjx8h2i*GHx;)cFV>iqE%Jl|_dk(2trp&+AsyXgiI|?EiK~=exd6 R(H#H)002ovPDHLkV1j$&y+i;2 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_scrubber_primary_holo.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_scrubber_primary_holo.9.png new file mode 100755 index 0000000000000000000000000000000000000000..dee06d883ac517c9b66ae0af0c16a15a173bf0bd GIT binary patch literal 133 zcmeAS@N?(olHy`uVBq!ia0vp^JV4CH!3HFy_x^nYqp2{eZbtvBZKw+=eziCqU hmi}G1c-5a8@k95yJSKB7odO!m;OXk;vd$@?2>^~6EMNct literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_scrubber_track_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_scrubber_track_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..359ae4a1b75ae5f9bd4c19ce86064c784baa5062 GIT binary patch literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^JV4CH!3HFy_x^nYq*#ibJVQ8upoSx*1IXtr@Q5sC zVBqcqVMg|9o+)h&U}t`MI=;37P+>~J%o_o$c@n5|5(G6;q{1B7hm1n1)PV|1 z;852bFr@$$7Qn88nKwm+BeH4P1d6k>yL}ET15`+IMHjoul;&W?0AN+Y(2-el%fD;v n3-oVXehs*@GTv>uJU-?D)v-F8SYd)k00000NkvXXu0mjfysLHS literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_spinner_disabled_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_spinner_disabled_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..a78d6c082d41e98b9c51f08009ef31bdac922b33 GIT binary patch literal 274 zcmeAS@N?(olHy`uVBq!ia0vp^59s(?EuJopAs(G?r#W&RG7xaR-?2k8LvO(&$$x*>KP`)y(dw0BY$)3O zSAb(nt^VY5Nd@IxANSauk}lhC@gpscRVSW7w(eQPzMzAPHg9{W^+UQ*MTK`WhYI7H zM(>wDw+a8@dE@ooBs_pQU+l`E*(`z;5dy94EV>^iFi126v+#-L&hnqPW4_I;Yi3ag z4{w!`eh~06@YIJ3TYb1xmhQ1-xW1=O|8-83XmPng^(_CjJLdN7KezK;8MC~(;`D5d RBOv!Pc)I$ztaD0e0ste|WxfCa literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_spinner_focused_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_spinner_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..e83969f680336cb650d5badd60f1b659a0b3812b GIT binary patch literal 374 zcmV-+0g3*JP)X3@}yO5hH?7z?prd=>);8(9FK(uTAX*xsu20k$IyHPy80 zTL-QUu=J!_gfj84?Ev_4vB1}qQxw0scs8U;0y%!OQd%&tAp9S6>QkfM4omSdKLn~X Uiw%mr>;M1&07*qoM6N<$g0Cc@00000 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_spinner_pressed_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_spinner_pressed_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..1b0c4ba70271c416eaefb068c5c05f68d1317548 GIT binary patch literal 434 zcmV;j0ZsmiP)F z_jWALjv~r}D)kd~$~+&JlVUIw5U!KU)J|CtX*lVrC`b9cjw;WGeg;TO4Hf){ z82F!o;THn~0|O%?0}s&_OgGrG5TfZn-@pHium1gKe1*>;3=9km|Nk@md-IF)7f}{; z-7-xlWFR(h{~P}g6iXQx{$sTOT_Gg^7Ri`lOgZ?6Y{3UATk;QTCZtvOAFX}Lh?Pkg cU@?ag0J}?Hy>ao7%m4rY07*qoM6N<$f}+N;k^lez literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_switch_bg_disabled_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_switch_bg_disabled_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..1e56c3253d4287e151539b95a02ad146b396ef3f GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^Vn8g%!3HEXp1%tNQY^(zo*^7SP{WbZ0pxQQctjR6 zFmQK*Fr)d&(`$i(g`O^sAs(H{KmPx>XP($l(RyUIWe0<@QTnqpGxtrAm^N?TKFL!q zyxlX6)1xM4Ys^~w&v;_CfTBUxhZP4Gvk4o_P)n5&W{z@n@ommza^saVt+0?d({xOH z*%nTN62`~>|NobikbL-5V5zFCGBfj%Kn{k-r=@cjrn=q)TF&6<>gTe~DWM4f7c@T&`9HKeLDBLEpih6WqVo~9PAS3`OkmBcr<56#LC!NH{~2d zziRyxYphcAneich!)uwUgh*C>&cs74)|FqnmWjJ4i_VMN99R71Qs{fO>FmO-so&$T Q0xf3nboFyt=akR{0MkJ|P5=M^ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_switch_bg_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_switch_bg_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..a2af2b5e20019daeee58ffe26fdbddd1dc1d52b2 GIT binary patch literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^Vn8g%!3HEXp1%tNQk(@Ik;M!Q+?^oIXnykaTA*O4 zr;B5VM`v<^g4`eVsE757rt&Er=r}XevUr)kYTMG~%k$fe{u>7-3D_BAeOPgDF&qDs zgC?4fXGmP&Qp%F@5r&CsH^eanMqM&AGH{S;WjJZcrzu+`bO&ewgQu&X%Q~loCIE0Z BGHw6> literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_switch_thumb_activated_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_switch_thumb_activated_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..8306fdd68438119ccbca29d81fed3dba40c51c0a GIT binary patch literal 348 zcmV-i0i*tjP)|41cIt0C`2EC5EK##?|~uV5iq<34vQk_3M>f% z3xeFV7q;0iIdS6tn=d75+!}g)v`cqm4<~C2sYv4(|2A|d zH;$FWPxoP2SCNhoEZ^{ALxfjA&VJ<234NL@nQ)vkKZ0n*{?hYUo+M%h5Qhe ukIHj@e8sIHrapPj{CljsV%baHxV~@u-%K7d%tlNA0000_n8RmuWUfY{ob#VE>@Z{N@?;DR>NZz!3wBK{B?~G+Hm*(C! zfB!eQ&+W_~;p9Iu$3Lz)d9lk?1_4m5KG)9IQJCzqP9#B>-GLELqd%k|( z`}SSy;6%6W6q_f#3{Cl@orf_LR!JLi*JMT`ec&~py d)W_umx9^zA)zw( z{y{&%j<2S);R~>%+6W~965~W23$?L)cX@Hio`ld{?#Tej4$KS}!2Vp@KI}41iQw#U zZgyl7nx?rc%kmMxb$~(u@H$P?Q(f0DOb9FH`o4e2^ZZs--^nZhfQS_9y50jAK)g+~ z*f0!IRn?nwj*R+7RX@gY6aWr{*nusw#jiC)CN1_~Nn){)#9||f#YPf~jU*NuNh~&! zSZstikA*p52F|2+9^U(7mSs0ZQ7D@Q0L!w7_nz*>23Xs+UDtId5xHW^0D!9MJkQS{ zw`ae51!nXI0D^Ge>n8yHxu44(SBg8wnHh-H+14XJtC*SL3vJI`uplST5JkU@g+MeB6+e8O@mb00000NkvXXu0mjfypedc literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_text_select_handle_left.png b/flock/src/main/res/drawable-mdpi/flocktheme_text_select_handle_left.png new file mode 100755 index 0000000000000000000000000000000000000000..39b74a9254fd8b2813c5db132c10439e7cc7391c GIT binary patch literal 861 zcmV-j1ETziP)W4?Q&vVh3o^Hh4AD_V^y1Zn zAo>?f7&5jUgsB%n6vX2WQmt$m^w+vJ2CT=5no2CQvffoYddEQT7-d|4&nEdAi z0s$cy402jx*k;LSAs&x+uS`qRe!ss{OPr=(sy8l|i%TRDefPtQDG+C`EX%J@9Z*G0 ze9Pqhem_5T^jJRK3`4WfIc@u5C6B>688uXB7lgXs_ z>G}6Fu(^f+2u6ql4a$2w9xfJ(owyrXNWp3gz~EadwS0enzwqS7p~1k>rAM#`hm9KT z?NG^wLLqS}nU15Kodnb7hOFuywKs}l%n9r9csjxSNT5(~)5Pycv4~1V+c`!f&t)wDgz9#fPN%a2bAkDlbMJ=0aW0Z;}|sx{S%mR2693fEhs nDBGHeiHV7ciHV7ciGlnE;T!EAXu%e;00000NkvXXu0mjfD{z{V literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_text_select_handle_middle.png b/flock/src/main/res/drawable-mdpi/flocktheme_text_select_handle_middle.png new file mode 100755 index 0000000000000000000000000000000000000000..d1a3fe9bfb889a641eb0958e90b139f9c3e78a69 GIT binary patch literal 895 zcmV-_1AzRAP)2}vcOC)0KY`eI|%rBYjWoDk=^X%*-`v8Lf3`!}9L?W`^ z9sTFLZbm63 zs;b7OuFOtDQR0v!0>+LGhVv&&q2a}JI=u?O@hYBY@|04dXJW9Y5c}gisCX=zrx_As*kB9`u4iAL$k#g6_VkVPWaU7@Z1lpEo zjFDtA8JoELYzD#L@5DEdz}TTn-5(-)X!vC&lUZ&zfwtrsV?@)m_{8OB(@n%T*ht{C z9vaSNv)M1UZMU32%kr`;6IE5?6N67@peSd+f15~P?BJzvK3eV?dD%_^Ey@2P9)Iuq=x^0d9FkQOM}%Xy4?(lNrcz z6pjOrhJ-+1U;oi}v2VL?yw2rvpR3g>R|4FX8yXtg|KRqUd)T@2JntIU=z99<-4oYS zsninB^Z@6!#5B!LluF$&%{J_sV4B^gX?6lw24FXv&MO}%7K;jWonU>vEx$ivnxt4P zDh;oufw<)>m5L18uAkHG!nRSVRAg|<^ImhrvMlCeeyg~O!!6@D4%ybK*MrpKaI)Q% z+*X9VUkG`>5b}N@6XAfWrWK6h-NgWqA)gmS0<2 zTQLmd9e}q0J^?VAE#j@b-Po*X7{*7#FuuB2%Yy}=1Yorhr)kdyE(9z9WdNU1Z%OQM zvDVH0tBt3{v&c=OlmU<%!|OJPje1em8iVVjb=&17n}*len}$Fj5C{YUfk5z=_yG$Q VSSEWYBdGuY002ovPDHLkV1g|^wI%=n literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_text_select_handle_right.png b/flock/src/main/res/drawable-mdpi/flocktheme_text_select_handle_right.png new file mode 100755 index 0000000000000000000000000000000000000000..476f702170d671bb33d3f237fe38a6a0114b1aeb GIT binary patch literal 926 zcmV;P17ZA$P)eGrr`cy4!g{J-;c?+nFsKn!HO;L zn-WUrwzf9^;fDQn8eEB3ES9xP>R%zvqTB5*Yv1{_^ZAA0NONOXZdU|?#2FAw5blWVa zfFeJyva&L8s&4WageV0df%D+*>S*1zwQKu7e)smvvaH%AS_o3vD2gKf7-JX!=QZ2c z4BUBfDIIQXY+PWu;dDZj&bqEM7{;G34Fh1zkA(|69-Qs#4243~7Wz$#bsw7~&4<@} zvg`QUu8xk5O+KH`Wm)Vgpwzqw1OiLW?A4Dz2=^3y7&x!qQyty0Y}J8S?7QBdNF+2X zL<=XgBc>3mkJi4w^Xke(sG*^u%!VBKM0Qb9QUSmJ+M{sdYHMq2l^rSUU0@{1jq>u) z-KO`q+uPgMdA(js^I2WozArsU_sO#st0%>@%Kb3q=4VVLrm6Y^$pv58NJRTo~s&*rrRiNv+OIEW@*MV$SfvR@L_AfIv>#gWNAMlYc86xtUJqn*!ni zoO*}B^38>r&H8WDeA7V`cW`iUaBy&NaBy(IFJs{`;b9`_jYr4+R=LIhe6cVr#*bLxJ4xx=SXtmgMsA{+62m@2c=JVJnG$|KBgLi7T9G z{AV*Ow;pE}i?*3VxctB63wEb)h!yN~i^7EJZJy_ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/flocktheme_textfield_default_holo_light.9.png b/flock/src/main/res/drawable-mdpi/flocktheme_textfield_default_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..47302c93eed96bb38d5d2c3945228eddcd02d823 GIT binary patch literal 151 zcmeAS@N?(olHy`uVBq!ia0vp^GC-`v!3HFKYIk}9sW?v;#}JRs;b9`_jYr4+R=LIhe6cVr#*bLxJ4xx=SXtmgMsA{+62m@2arg17~gHw)$-e2bJA> z6J#R{Zf#${nUIpu(4sp_L7B1E;A_RzL}PJ=dj`{h|SlK*cp@??i|z6Ma={V#LbY?CxElyP;457ZVdS+)*!58aZ~6SE2KGn`gnI z2^q<;OHK_KC7}8t1|kQO7R*49K^#h@B}v)=1c)FH1s(}WBq=FDfe^GFma2vJkW$w4 zmKI&**cu^$!t?cdovWufKOE*!Hk*w(1R+T+l2OwmW+UlE+u95oj;zoLh~s-8W;6%= zagt-Hr%NHYfnh8Ydr>PW$JLI`mS^QO&o;UF?yfmSgD&XFIk6MHwO_rJ*FijEs=1qO_Vy3t6oo zqg1NU=2!_U%35C5ksz!2EJ6h&%d(!=1gK{;K~3qXjV*c+F+B^nbsehP!cx5tU==mQ zCh^0n?~k@Cpj7jTAJzN-s1*?yrsa6?I=-HzXf+%T;%-o#q{b;#-GR1*RFrl{Hx~v|KRzfxzPtZ=U*Q)`v1GRefwC?t6SGD gUp!wSKTNGkFjo(AQ+w`!$@m9QEc9uA*S*hdNlNIs`@0DTpKqyH#&=*E)!Y24>#J{C~gS|MOa|>r7+) zp?Zd48Z#YQo~}pe%kA4sznzztGjusfQoW=H7fDI?5Yukre#B;MeE{W=ZjO(2eL91iJ zLp>8+1!H2!h?`v7DYmt&Py!nfoh{qLj<1vx+$OI=_rbNmv707jD8X$x)tk++D)tZ? z;X%j%QNTta52bJ{7CXs`00IGs0u)0qrbwa!09$!Ds^*zRC9kC`TJ)6Q1_^N$K`50< zd`aT5Hz2@xJRWd}Vu)IV{Beiq<&fjIR2ekn8=mD73p;GUsQ2ShlHjPPTOrtPHoHyi z_?1M_lnG_s6(A1;+YaK|wDw6J{dHqUYrin=A|a1_Jn9*=9>ta_nC5PGp}>%OqjY-~ zEs8#@VPn)r4#{WJXM~_R{%Mx#745)ayWRTqusuA;p%dfL(FiZ^|3JrTETgp-%J073K3D&g zZBS=F^*#CBC>{Ma@N9a%>FtS+-R;K~rB z|AAFMuX-&iumUTv0uKTfeGC=fq4GOoXo21?WKsiTPN*1qKm#){s)2_qg*$XG04;_t z1J+p>HmmpRA<*@&3-x!D%24gfWG#mCMxag=*MJRh+6YW)33vf+JF}z55dvpxyzhjC znLzw+G5UssU|Cn|5PjtLq#QXIRB`)D&Y$$fa-Y_cCDV_~sA~WK002ovPDHLkV1lvD BX1D+V literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/icon_calendar.png b/flock/src/main/res/drawable-mdpi/icon_calendar.png new file mode 100644 index 0000000000000000000000000000000000000000..69d1a1c32b4d05b8021b476a2807a1d37d87f207 GIT binary patch literal 425 zcmV;a0apHrP)Px#32;bRa{vGf6951U69E94oEQKA00(qQO+^RZ1{?ws3hK&-+W-In8FWQhbVF}# zZDnqB07G(RVRU6=Aa`kWXdp*PO;A^X4i^9b0TxL_K~y-)<&-~8f>98}fAgdO6Izd9 z>jB7j4b+x|))*7*)Gx$Fy#R@YC$Q0k7;7sE4`HVUgBW%!K>i5@fzC-@GrMnhcIV~6 z0=QAG_@bO<`8cNH6Vjr40&vbAusLGi=#U*WdA_O z@edVxTu>`341_@aoEByaQBTlc`uzn;iGs}~@LEt%C2=FuvX+-B4L=6)=%D%o-_m9O T0ecH&00000NkvXXu0mjf_vxWz literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-mdpi/icon_card.png b/flock/src/main/res/drawable-mdpi/icon_card.png new file mode 100644 index 0000000000000000000000000000000000000000..8ccafe2f5f15cc233d70fed728e03f4a55c55490 GIT binary patch literal 410 zcmV;L0cHM)P)`f00001b5ch_0Itp) z=>Px#32;bRa{vGf6951U69E94oEQKA00(qQO+^RZ1{?wt8t!JM{r~^~8FWQhbVF}# zZDnqB07G(RVRU6=Aa`kWXdp*PO;A^X4i^9b0S8G$K~yNurIS5M15p%(zw?;!hoB&s zK+?a~Ew}=)Fs(~SnZ^wyg`$wc6}W*^no2FLTtS)v2O~n7Ng|_Mi;?jMhRnoMzQ^I7 z^SyUv;~R#@%qNJl3b%0U9A3#T8q6oun)oBkme?ddQpQ7)n9YW)2el@hr&2SGuYiLV zN$e2#cGzh|yNizJqv*IB%*vKSk6{bwaV0XM#qDT!Px#32;bRa{vGf6951U69E94oEQKA00(qQO+^RZ1{?wuBw$7@k^lez8FWQhbVF}# zZDnqB07G(RVRU6=Aa`kWXdp*PO;A^X4i^9b0Ub$1K~yNurP47gM^P9C@ZY&%QIZV4 zfhdF2e^Lg!)o4(NSfuW5a0in?84Na?FTiFnn3)X9BC#<@j={OPj_Ysm*6F#u_jym} zdEO^zRII=-wlRy|bS|)o+faV-gRElZ5>q(EP2w!!C6xO8R_{G0s7(gzxLcr*dFFF_>04Dc*Zd{ a@cIGDqH4A(UEaa~0000c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxKsVXI%uvD1M9IxIyg#@@$ndN=gc>^!3Zj z%k|2Q_413-^$jg8E%gnI^o@*kfhu&1EAvVcD|GXUm0>2hq!uR^WfqiV=I1GZOiWD5 zFD$Tv3bSNU;+l1ennz|zM-B0$V)JVzP|XC=H|jx7ncO3BHWAB;NpiyW)Z+ZoqGVvir744~DzI`cN=+=uFAB-e&w+(vKt_H^esM;Afr4|esh**NZ(?$0 z9!LbN!`Ii!Gq1QLF)umQ)5TT^Xog;9W{Q=uqotvlv5A4HlYxt&p{t>#iJ_&diIcOV zo2j9>iIX8ruSMv>2~2MaLa!lCy`aR9TL84#CABECEH%ZgC_h&L>}jh^oNh68 z#c3W?ZwgMg7`x%rs}FRHJ}7FDq8cUyOg|tdJmCU4@T8xb2Tbopz=U1A>}3c815=Bq zi(^Q|tv9pw`!P8R9LxVGlI@(FdgRzCF5d|gZp_M9J+*V6#EgK4+&>uDe0ZkzPHnZ_ zXw1>{+(%*(w^K@a`a8cFzWZA46mH+W|L3>+4V{ZLBAzCRG%y7)a5*q)Ft9FQ0CIlp zp5moCyF9;fodfHI3ma<{S8!)F+!L=|artFLs6wEb_?e%eQm-jlsvOm{YI%s`* z#&;h))X|sGWbA3Bn9{T7Y_aYI>5RGSY8%TOM58|C%z3cYsztM> z^350F7nQGmF}_G~WXSP5vDD<5^Rk4B^PJnJKKk~pX@&Q>Z|hbwPqUt#yLl~hL4Le% z!nX2Kr>VCz%viW2U;oe%VD)4;T+fi8!I|)~3(3nux@qS+^EV{uHEeCsWO#9T=YEOT z2JaqhV}90?AI6yQ>2XHe?|m=tU1C09q;ynvf|n;VgPQI;$Mpv-^eq@Sv0M38Nwo0Y zH|Htc$Fe^-$%1oUHIu=%+D);nyL^mGHJ-RR=ZX|NG%J~ScDnZUK8a)dXz6psj^*)` z84?UD9ei^~dle8wExXyI` zzPiPFVdQ&MBb@0Jf7l A^#A|> literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/alert_warning_light.png b/flock/src/main/res/drawable-xhdpi/alert_warning_light.png new file mode 100644 index 0000000000000000000000000000000000000000..4f11628f30c73f4c0752a627ebd1a67eb895f064 GIT binary patch literal 1813 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 Clc^dkDteqxtw>p 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 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/content_discard.png b/flock/src/main/res/drawable-xhdpi/content_discard.png new file mode 100644 index 0000000000000000000000000000000000000000..98c73da1f35b06932d1a76de6468afb2768998d4 GIT binary patch literal 1848 zcmaJ?X;c$e7!430Q7Iq-qE?2WC|fcU6CsfZSpWkB5h8|CDMK6uuGVt(JlY^Hm)M)k3>q-#f~WCl zpOTVEq5vU^Apn~W`pUtX5HOQPhgcwo#qj}{AOtcXCIj^Ig+MObj|)P;=Z8i_!xYKf zXi>=LSj34(OT}>wm%-5KbadSeI*O$*AP$Ga0GSLX)0gn@)n=-3SnsRWx(_jk5Um_j zYH%g0226}_5}JYYXhf!;N>FKrY1P`#Z6X@R(8C%AL|&4SQ?xXyi-2 z|4FQsW@->dG@?Z_FgY=}WOq|24OfUEFpgqU6ipxMVpJ-MquNwd0|;Z-bO?~Za;4g2 z8AM1VT(Me*XsyHh$;Ub!M~mxm@dIcH_wQ&GUlo*REN8ZQG4iS*pV> z_uN_wGQ^uD%vnW>luoy}+7ewyO7Ht^McnP$AA5Rhjeg;VsSRTjsRzkZ>MBB?ltxV# zWvX^)FoU=q$Z~rniC%NAOHWYo`+B2q!ZQ8gyq8#jryz%x6=XaQ^$RL98W$GWC0O1zdK;VcYRPD`TIxzaYBjIp z-JIhr6Ffilz+%aAqsIDKM!e^y z8ZKOtPLUH=m9zOfYPk-8N=x(9zbKusxh`|F!iSv-er-q#ls= zP`xO22?HLm4rtEg6{g#C&N?dGeZ{FHNZ4?jnh@HoPJA>|UK_vR^y7jpd3}dM zzdsUIJgM}ZSM0%2=I7()o$wfw<5;&WEM}b!@cv_3<&|FYGyDDaE$*jN%v)M6OmpSO zxmflnGWR;|bcl@rg36ECQs+`rD0}RkEM1muywk(CvYnp|BPDG<3+9O|o4V&3ZTC8@ z4>O)MbeJ={i}PL{*08(U)$}WmyN$cvzAqA!FZxJ&-X-<- zj~k~c(_2Qixs(Rhc~2QgwVb3}IR9wnCW99xbxHf1Fk8>+qq#0piF;6U1hpf((0qDT z-463;_vU8NZvM>==gce;mb*_S_T%l9aF=-9Be<8HzQk@=+wP4{c3HUycE zb+zKH_imW6(Xi<}D67qHBj0)=MOTB;)azT{8ypJcP4zDDxvZT|^d=d%i*DhP2BPM(3e|qe(Q^`q83+L?mTpxFbvQVS* z+Z{+rJlsMFUzku0KOD%vWL7eYqcwIsIVm`dj z5H}E2SR7$!zIxB%B;|r^z(0KZ)4sgsNjcqR-P4&?YE_X5+*pt_*;U*3(dO2ow!!NnGxsseSA8UpCnefGaLNVRzcKyG N#KDoG{er|5{{R}rz~}$~ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/content_edit.png b/flock/src/main/res/drawable-xhdpi/content_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..1ead8fc3319463492bfc5162c8ced044e5b85c05 GIT binary patch literal 2343 zcmaJ@YgAI{8pg|*SmwRbE)rrFLpCU&Y{d)U1r;^5LTyrlt&wsOP*Iz33@wxDs7aZf z(rL<0X_ixFM@l1`YO2wiE>;GT$F#CcE6dazXtFs!PG_&R_xFA8dY|XLJ?mS$I65-K z(!$9C27_7BLuoPkHNfzio9VyDxEUJ#;wTAXN@4{ml5Ca;f(3E}>mdZ4$4Z4_AQmSl z;~L}(gPEYXaZCx55dpFVJPga=gOT!tdNd5?>n|0u*lCaiu^vk0^2x}dmR2N!%ONAz z5Exj7kP2X?@WA4*0L}xzdb#7UAi)d7;t*dR zq&}L6lLE%jg1^MlpUB7!5{VE5fb8sSOtvRRAW8*rBoYb0dH^0C?s^Y*aSmU?lDhN7 zF4GJ&NX!;-g%YlSk1#N@)(bKvWTZaRze?ZU)rQ^7$9W{0UQQvC~4Y}!TA5K zJlBAbq-v(HjI3fp~*J zh@i$2FgOH*#pd!2md^+V1EllC5*D8g(P?C)o&&?>azH%An??;H5C{}34TlTDk?=TD z5S~H}3?|^b@D$uMmL_0l@*uus8q4_`OZ+C*AO@aLADIS;xLFVC4r{o$+hA--PkBE7H=Jf`-;VBb~JX@POl zhl5p#>GqlSzouqC3adc|*$l6Cx#Q;4z^k)%5J-!QgXdQy&(bW4W-Im#wCwV8+_C2L zQMi+x!sw@J;EvnI#N*qfRQlbG1=fM~?k$f_XyL8JCNOv~-E?ok3VC^4`{Lx__$6!U=%ff=|04Age|VHd)m58P^!u_Zt3b*MT*2J}8?#?-@%ZIh7HET<2D0?@Lzn)hM-xl2>sHq_I z`Rguf=QL%UaB!WQ@x7P4-Zg2zQ>enmw|nGD)_rOlI`^#5ENm_HhBmTg+y$o%aePZ> z26{6e3cx_!c3B~Sf;LNAHWmEJ_GVo$n z$~)w>+|9_I)jGTm2MrDa%_^NJDt`G) z%|O#G9BMi4qI2&#Kqi5gw%hh3e%fdsrR`SeF5G+h*m*#MuB^Asdh^dQsp8tW^b$V$ zG;Kxl7_I5ds^o2_8l_f=i)4LgmYg(wh8oQlpRn?#2l!o2E%Qdzaq8YJ%WxLARt}ny z!Hahtm8jpXqmTA^mSXp`_b>g`GOqX|3`dp!*zaU?nCWK+PxY{pHct@Cx}%wgYSWZa z2m5;m$3BK`+k*F=WchOLth`>kX2y!VJ!Ybd|30~h_*_qwLz(|Wz|o;MFDOl$<|?gX zb7ZF%t_Zmryzs2*O6M;n^bA>-=^YY3M^^U(v98XUXkXm` zw5}6wgEhJb5mEf1fw`X79H^8@TEb?PY1hVM6%RJ;9;Xd-&OX`LSh;ylFK@9@>v`+9 zJr8H#an&eha{JhWb*zP#va`&(n{igR-YFU%_qk=V?2_}wdsjZ3JDx*i-MWEGj3+Nv zT{544vu5JWx>DK07`icd4pYIB@StMd(A)GqQ$^VWQL87E#G|U5&XPF01}Aguv&??x(rcrM#;;zf>?r~Bw=jH5o+w|o+WI+|scf~K%@F}1)o)6wCEIVOyy7(+ z&yuSwMMe3fW?7knzg@QL$@z7T%H0?nY?bZ38`wX#mIwf0dws5P!f^gOW?om7ysQKk yo-&FYNhA`=V~}U2NZ{qe!2yPgF*fm&T{28oDr;&zQ?cCe>p~BTq@AE77yJp2i^97A literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/content_new.png b/flock/src/main/res/drawable-xhdpi/content_new.png new file mode 100644 index 0000000000000000000000000000000000000000..9b48a63dad67fc8cba5cb57abe12f5ce97dc9e01 GIT binary patch literal 1225 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+m{l@EB1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxKsVXI%uvD1M9IxIyg#@@$ndN=gc>^!3Zj z%k|2Q_413-^$jg8E%gnI^o@*kfhu&1EAvVcD|GXUm0>2hq!uR^WfqiV=I1GZOiWD5 zFD$Tv3bSNU;+l1ennz|zM-B0$V)JVzP|XC=H|jx7ncO3BHWAB;NpiyW)Z+ZoqGVvir744~DzI`cN=+=uFAB-e&w+(vKt_H^esM;Afr4|esh**NZ(?$0 z9!LbN!`Ii!Gq1QLF)umQ)5TT^Xog;9W{Q=mfr*iesfDqrlYxt&p{t>#iJ_&diIcOV zo2j9>iIX8ruSMv>2~2MaLazx}jh^+-@<% zX&zK>3U0R;;nb@Sbc{YIYLTKECIn1BASOKF0y*%cpPC0u??u3b?R9%*DFXwewWo_? zNX4x;x1+h597K+N+}NV`x{&oEUzVlqt~pT-0)g+>E$2?XENNh%Aywd~eCB_1^Rwoc z`FjdCor%g0@&jd0FsShj6p^|=+xzUSm8WB~?)x+^TeWJww2-p%%B5Gf{H&Q|mBsL* z$7M~-9l;y3`X@Dd0`n>+_-oLM-}BfpSg&k8 zbZ+|E!}s3)@p`qYK=2Ju!2ey7*mm(+wM1*3XKdb=!|mUM=?sUW&Y$ZxWJm9>YBDor txSkv{`}_5$uO*}p$*VyXE8MkVm0&RGjTPG4(K#0+=;`X`vd$@?2>_7}l579~ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flock_actionbar_icon.png b/flock/src/main/res/drawable-xhdpi/flock_actionbar_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4dff2c9ca872a82052d16502236cc28f5669e8fd GIT binary patch literal 3441 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 Z{y#z_ke}m;l1l&p002ovPDHLkV1kcgk!%0} literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_off_disabled_focused_holo_light.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_off_disabled_focused_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..5f900e1c0ef0d0804bb1ab08ccafae6d36944d86 GIT binary patch literal 466 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEV4Ugc;uumf=k2ZizK0!Tj(%J( z*cG#@OJMC$7I*jl2nM$Vw*Co{5{;Y!wzB4qF4BstrcC2@df05Du*3eNzQ+5@Die#o zecr6E`Od)w2%ac}YBrm3XD!g1qZ%Gx`ekGDBJLYH7L4l}#AY1ryFZb!f>CZ&-}M=d zfvhdo^~^CxdS+h`E|CAMBI7=90o%DlzfF^0|6afihX(cbNKGG-n1Km|pPl`HjB{-_Gq!|8SE1gqFgJM$yMpW&|z!q;9cyE4VXqm!xXnCq!Gt*4F@r=Qe1P%VG$`P+*3)$1-FUoK+Ma=q-% z%!&If7j}Gnbo)c*kDuN5yLA_^PdhNDkk8NVWBo>zqNpV~lV>we&oNhd+k9U7b42N$ wqdaGBYFH-M|8u+FsF=EVNe2*|X#B;nsna!&lWoIGU|cbHy85}Sb4q9e0P1Vc?EnA( literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_off_disabled_holo_light.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_off_disabled_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..6a364bbe3c3e82381f4d415256621621be92e02e GIT binary patch literal 375 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7_5;rX+877l!}s{b%+Ad7K3vk;OpT z1B~5HX4?T7Q#@T9Lo)8YyK?ML znIuvre0;SU(0!^Ut`Q|Ei6yC4$wjF^iowXh&`{UVK-b73#L(Ev#K_9PT-U(d%D`aj z%_DD6H00)|WTsW()^MbyCIF~G5@bVgep*R+Vo@rCV@iHfs)A>3VtQ&&YGO)d;mK4R PpdtoOS3j3^P60;{c#qa7C;`Ey>60e7zBbgHaBYt2H6zK5Ut*uc9;iBFSiqR#aJ~F*-;@J9 z6+Uize(QYEI`H>X_<@<{cd_kd`fgW#sPQ@bk^`O9e?6~ONv9l84BaMD7S529|NG`k z=Q{Q;4SomgN+TG%x1W9_b%L`(#hL5JI`%tE?h}8v%wje-ls+PMVd1Y&=MSh9>BloC zcs#i)9(~~05{<6uv%dYBZgYO2mO|In&9|kr4p=pqiYCY&n8me$FTs~Jf_=j>rZvnF z!Hm}!*Q{iC&2TNGp_CyTm$KEba|#}udo{~sMe^huOkpAJSDdpfR`3{Hn#~AwBsu^Ya|` zNw*R{AGT3dwyP04#^Y6%dH((jE)8ckRaX}v_;y8ns+QfaCFdl{fpNy*>FVdQ&MBb@ E0F67&f&c&j literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_off_holo_light.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_off_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..a1c2005a8d440ec53c4e392a6a1e546f5ad43c86 GIT binary patch literal 293 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Z#-QbLn`LHy|poy$x(v!;m4Dr z3eyX3UUP6=cUbhMZjPH|hmarNyAz53r_R?sKV4v>jhp+H1{KdqDxUw(c|Se7b}Op| zgS4*h#--{}2uP5RmcGap?J=Pf9I zxwqce>b2rp(;LiFmelRu-^O@}`4Z0rxd~|vUm8r9m#{C9RIpVz%kYvhlg)$Q!`Pw9 zVHQ>;x!3MKsJe6Y?~>AEidPw=e_#DFlT&;u^Sa}0iB}oSzy1FCLSDsbzAnhMm*T(j YMtqaFnNpZ01oSt9r>mdKI;Vst0GbMW-T(jq literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_off_pressed_holo_light.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_off_pressed_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..7ae2fad2b9779f675d11f1d1037db2ecc9cfc42b GIT binary patch literal 549 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEV0`20;uumf=k096uG~q;fbOV!D!ogZ22F4rPbAPi1FqrsmX1aDj{lMuj|7_)ca7-zUy<8ew@Z9)+ z!M_B*F9(h>dp|4xP+;|JD!bO!&Iy-__nYtWj=1xg_q~!%LzF`=YXJ9(jSN|greY4d z3)EhRC-huw7nr80>gn-3M(6aW7km4!pF6=N;1nUp*RHR*H1yn@?80+z8>g>Y^$jF3 zA?TClQug-TxawTN{e{r7-+riqw9mYb-Unx?;AGXG5Zd zUHVP)A$4(0>*YK@kN(J$Mv~hEn>9DCog&;K%S3zd=ua94R6m91sx?;z5cO^`O}Im_zVGihv%AxUOeT}bWHOmd=D(yC+VEhFI?w=S zfJI=z?AH^=RQEo+`<+^gHz_-7M6|7=lRphy1I`0;31jgKSugmz1Kdy8XLJD;f$PB4 zltDP>_dgH%n~b0W82i`Jw!df&?*O07=Wc>nijwuWw*Mm$nL`WnLn##2CiyF*oePHX8u;q;z3lXV1L%mzW=s)c z9pGX+nFF?fpAqLO%P;6tz6s2m?@j`@fJTfHVjVDupGv8%MCTfch|JJae%|}~1lkF^ zz=x3Yr7FN!Fv>USSKg;f#H5`3vQiaB`NNX`4m<%qhMxDIaDX%6EmNHQK05hFz`K}9 zaOxcpseDxOFq#7@FP>4p39OJl?(wV z`6fDf%TaS2GaTAZ`z7>3OVF4Xk>kKQU;~s*yu&mU062zzM4c`8ZXT^NBJ$iETjKCB z|CoN|7l2d1TY8C4hXc?7KSe*CodAw_-&-ft(E%?rc+7tX)`1U0X8Ygtk`KDlSSpzV zOrV?S@%m#xvw3=xV^Q+ZQ+`p;i?N9jqh z(#v3=6N(5`-r`>of~ovgIM4}}SmiCH)Q9o~i0UZ939-r-#WOYslqtd>C+q{CiC2DP z#`-ik#0g)`u~_AOrvGu+bHX-S`A3YVe5vsd6Iu^&oUqQ}H#wCuu{A-H@ViO%Z<*Q^ zp>dH5FzEm$&3|8jRFmLw5lM8a@IF(VB&GoVWy-pz4V*FCxUnxxly-8g1Nw@flxmB} z%d(xPKq=McPT!%*R)p+oi(PR!d$7hm^Tx(RZeoDCN&c+acjF5pr~tJV-Geo{=ymt^ z=w+@&;8ILyW8ghH#$aL{u!DYR>5%?4xlw4NlYfeiF__o_>;YeZhgA2XWGr@Q?xK?) z`kyDC;5S8~?{ztMgUJ=aT?qZV)mq_mQd9w4Ofja0n4~kAOeT}bWHP`Xz%qjoAI14D P00000NkvXXu0mjfjFs-5 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_on_disabled_holo_light.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_on_disabled_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..d705b42043705c732e452228301f7ae8fd0304b4 GIT binary patch literal 762 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 q(zp^yQVu!fkV6hRIP zhkH4@d(Zj4-{btwxdC#>A%`4t`2Pj@Z%<&P;ID!@^^^5r!+FmClmOXCXTwm$xaI}0 zkhTioBnWf5&nCt-Er5ai)vh|&RjxK61ozYYcA$}WBJ9+!!${a_aYqlhCIxUHpRK`g zHIkOKeyb2R5w5uqu(&;iY9yz7BFq*y zk%n2Q42HF=e4B8vyiV3TNXuW}R#ZB(vGS>NS5Ro^A51LM%HqlcLZJ`^P-q=sNbm>% zp>&*|QwRc9c2dFb_Xs|pAlz1vNUoiDFo^*H^lOHcO#HO{Z0b9ttZu{pJG*aNy?0sZ zCc5XvlJSUW-vtJ8fl&mL7r4p+p-`v@!0P~tM)(M7oB|~f1P=$LL=XsvK-K?yC60w_hEKn1@KfIF6QwB^xD$X7fOCM7u8-T=nt>sgtJWpV7^_B?_X6-6`U*CDe0{KN@yd0Z zr~RdhM`IvTulfWoBM|$naO=YB?w$S4l=@}UProvTrfl(Vg*yQ}c@(k;fH)B!O4;g^ z>7B@X0pu^*;VGOtXX%=^?)j{WM|wfhG1J4ubQ<5BJHceR1O$Arf++)60&tH2B8xOn zAi4#O#GD4Ue#c1wW0uzi@c;7A^@UUCEM4<#S!;6lO|u0L|;;g1%! zy_>I&WGjWOa3w&{E74e*03^dJTghtu%=#T&K%@KXk3s+gU(ERTN}jlABcGB~w|}I$wCN93t*;d3XDn|mg>~3OaOD93FQah) zd4jE8w^m|SPsk=z#{FMtpAaYyEqLUmJ5N5quXGP&dkvBTgwww8Pmj#(+BdIt`;7(Y z!@xQ}s0WPRUVu`75OCtjMqU8mzkM^N2J-!4$CIhc4kB0u@c(evn7~c>`MmFb$iMfO9clRnooCXmQQJf^{dtTMiHDTxS9hCzS58?WA=&{QIflZoLxAEqk zcgSHGgo*Izj>04$@g;s$*1zmIX`z4fiGv?`z(7l8KiF{JiCTX7JcuHho~7x~0du!SoZqZ`?tT;i4_ViE1fb|D+|E#CNqjDgkaFRKBnK3cRKCC(RM$NwIyDvm( zxZlhNL`;CkyYWWFsRzd0dRNU!p?b;A1?T7U#6>OU2j;0}dvEyB*0QFZRqe0c5*%>( zLy0L9MF!@EyjuYnzyWUWFBy;cvE`lbK6AYK@Yj8Qy9itXiJZaXC8r+OsFU{F$szVwZVJmA&?n4;ZeR4^u-cw?w()2f#8KYy9@ zg=Nb$2$O*L1t4}oy*EH+KzG6$e(3L|%{!~wUo&KTT>vqX&I^bNF($iVlLuT1kdi$z z0LY4S<#k;ik{`@Dy6f4_(5drrUs!RRj=8g6Qa3h_AFk|bTyVU>YI$fDPf1A03fS@f zVqk>ifl=`Q&cKXn69|ADliD@ki3K{ZR@7zV7pepFTSC zcr zTMSx7NCD_84kF%I+p+E0&RM6<$9>_n45Ve?V$vI~@2EV!Z~lpG#X-a|h~@R?$}(f% z9Lat@V)Cv80Dx#(PzP6q$YyHhnwhhj3;I0x>lc3Q9fPvRhfca&F4DHBkav@SjmUJ{MI5QO zBM5-~#f5c4V_WC-F||32816hk(Rgz1boy%Cj~mHnURTYTET=TB9G6hb@6Fp+vCq;3S4)vV#-(QN(EdfkbI26jJX9MfH9H5Q03)1yBlbN(B$6AX>kN3-%YM9Fjt0 za1L+|Ss@IBF)#*+sr{j9ZnS*qXiVI#01QAFz&x#~6V$>}QUE}q0D*D8XFl&43|t>0 z8^Txl{Hky(Km@>AIE9i&do-bxQ;&9TsCq!qD^gGo2E2d+OhHuiYROIY0>Tt%8?9{F xo@sN)A%`4t$RURua>yZv9CFAZha85E{{Wr4EwAI%mR0}&002ovPDHLkV1fe$8WaEk literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_on_pressed_holo_light.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_check_on_pressed_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..db233e7025895032889709cbd98cbc604de3f312 GIT binary patch literal 1134 zcmV-!1d;oRP)??Y)8^s(&)V&Rude@n`o_-W^M21D)IEb@B1{G8A$>qgTWvG zum+$5z{B;#05}D3G#m~S08juh7!1|`ybGWUKsrmWc_aWIK&RjDBMie+Q9A#AdEh3I z0XUQZZsF#M2PT@=B!IwmCJyOHT;Bn&3f^@d@G7w^fa?TYvn+t?1YEN$fa?TYvn+t? z1nNLUUXmnt^E_XZm7KxUR%mfb>2h4efxQc7jOM0^xQUl?P)$+GOPAPAoOzJD}x z%sL6>d44NNk{>YHM`#?a^-n7+EB}__w~aC10az!Z+ewl@2=SZNI+`VB?G6C2CZ#-( zQigS6RY7Y#D#brE#ykSBULIN_qOapPmO&8g&(aBVSqMof!`0Q*gL%Yy6Q%gAbK)&4 zMD%5nB$-m`7kjI`jVrSfEX99rjM>HO;$I?!s3hESC7uGM_{}NKTOy^@vu?NhXI1xs zE6@R@_?^=EyHid(Qc67wf?!{3oy-z{p*!G=^KTMwo&RH2@xTIi01>EJ7bHYXm30oPZRUX z`EeXSc#HVCIB#jJ1J2FZ?xbmYo0*>kK`^?g6H4)0#+bb}oVPU60cYmw_POmb^JbQ1 zyW{crwR@U~WE{u$+jQPC_X04pEN5(w@#>_-W9FSSO&=#ovd+u^UW(t!^Za{e-k1{h zNGbJmInO@-($5Tna!$A4O5r>gTpZ^x-_>)GB}wf&d&6(LEx%H;;s7 z-|{1CT|@f1Zk~L>cpts0hjqIW0AP=Pl06tTqod~WdU3#;F@It zTqod~WdU3#;2MeQFD$MjMgnlQAGZLUN&qLA{6>1y$N(IvFbwm4zYhQ!<qX7^B_-{BIz5sy#0W=^)-X;l_a{vGU07*qoM6N<$f=ode AE&u=k literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_default_disabled_focused_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_default_disabled_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..8e73bd42ae7d6ec0bd94a4b6f297d7a2cf75c2fc GIT binary patch literal 378 zcmeAS@N?(olHy`uVBq!ia0vp^W)+B`s2pGEIiB6{%-JO{cbi^lT7Q5Q*&MfCpV4R6-FJT#nf_{~M!rhhyp!z$#YMO5iu~PN!O~7wpavwRh8kfTGrZr-?_T+8{>|rtQtlHZjJb`s z$L%^Z$DnvZWBq1#7tK=IV8+r*>F$K|MF{rL3 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_default_focused_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_default_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..b854f6d1cfd534ea50f3435426b122f051389992 GIT binary patch literal 487 zcmeAS@N?(olHy`uVBq!ia0vp^W18Fy7)M8t-jYr1EMYpL;yPHyY+Je<97OX;_p z*0zaX;@G%~*S$2iJF(v8qJ_1&>ARRj`;E63OS?4}eS1C6^Mv^Pvj?9pG=9l-!zys| z=~~Ta%5xv3Exx%lpzEfoDcj{DJK2t&$ay<0)~Oaft$B9Ze`dFRxQX;};pFtY-j_SK z1R7?xXifXN%fvp!M+#=W^U);>lP(1qW0QJG7G1F??e7*?i>aU}I%Kf0 zi}m)#f}{Vc^|C(QIeO{gRl{u0DBJIuGb%5?5{&eIH2Lexh)eRHvJze|J-q6D@8x3> zMb{=pWHj$))89CkS1P>!$YH_PYKto~6*Zr|4AD06@LaI00xh*MoL3Y+2U+&uu=8a$dIVY}|VbUaO{GiTu*3IwVgZ~x>FVdQ&MBb@0L59@~ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_default_normal_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_default_normal_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..e60160eb098069e4238eb0b44239f80e140063e0 GIT binary patch literal 386 zcmeAS@N?(olHy`uVBq!ia0vp^Wt{pGxK*Dn8< zCt06z{>BEmOY3u^FBe*RPQMYk+k1Q)bCld=56Ncsy(;mOFmAWuQflbY2EU3 z)p5C(x4t;F$+xjv$(et)={hNF+)Z*_kcdr+F>yoxtt}&XC_-55-GYhNZ z-nP!`E0@$*>GrgnTYR{(tNq&>j_VBHq?T(Pz7o8o^m`B3M!~Q%4Nt$yO- b|9kSHo>mVlzRV~FhAo4qtDnm{r-UW|+n}rz literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_default_pressed_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_default_pressed_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..495e39836beba96a012832f6e8c2763986fe15ff GIT binary patch literal 373 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 aPYp^o33?UvSgpl^&m%UrO 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 Y3%iUdRVs!;r2qf`07*qoM6N<$g2v641poj5 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_radio_off_pressed_holo_light.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_radio_off_pressed_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..0bbf8a326db1a895f9880c7f4cee4aeb3565ffb7 GIT binary patch literal 2235 zcmV;s2t@aZP)9=B%l62cf7OgQ#Dl76?+1s6+d8QwBR=UX2>2CIH_gzh3-=z}m$#x{U_Fc|204BVOkFZfy1!Z)gYlxT>^0Swy`5lnzr^## z_AJ^EP5Yr&`6p2eFK3qPV^5#1xAO(M$uw_RplX66-U{x*OW=UhxgcchfJ(llKo zqIrbD1!^tW(XhQ5&;&NYaLz$%y<8Nl4s=8goM}zOb{xkUGp{rAHEZq5%)G9Y z+Gs%n0jRPpD*#V`_xPTG_nu~ja}J}?=;^Aeeg(kBar{oV+s%l`hR(8qC*m{b+?uua zDl>lol3uh;uB%QDwEIUTgv`&#Q|t@T>~D30Ubbi3WvY1DmM2qC!J z?XJdg{0;yr%kr(VESFjg+40YgmlvGa@J8!R=iCJ;T!%&S6(VUi>_=X6$21QD4eNp1ots;YX`7!&HV!~i6rMJ88keSw*; z64Cofl58CQi}@gqT%eohE+FG-U1z)1unNs{|Qh;?Ru!CD)7g2jyum>I^HB>;;; zh`YzJ_xFN`j1b~3fW@k+LXSilT)wi_E&}Kh(fz=G-WpL9{To2nT6;Edh;Ra&a~&VC zZ3h1H)*zxy=UnV_3t>YboPZdBjG}1fYRtSqL3C&bjkM^ht{}x+c__(x7c1rTox2*R86m=bSsX6NQ;&RaGwn=twF57CH<(0S18t z#BqF&i0&}+^|CCN11B;wwARbk+UrF0*Eo(tJ<%Nkntr^QA*C#&l((I8XUnpD<2Y;q zGt06p-vH22O5JWzIMD!1lw}$Bdc7tmICH91O1b4TG_Nu9eWldrAz9#Q;4{xxthIe2 zx^?EvnZE>v;f%VKX1m$9IRdu<5t&Y>b3+Jm&lvO5(P;E+$T-trG#Wi?jQOz;Vy)BZ z+-MPnHjPy~u5f`C-c%gN8*v=J3jkW{U*>s!X`V*Wulyyg^{)YNk|ggYNzx_=+u8OJ z0QhYnQD=u{-UdPlNGTur0>%|7teVV4LEea!y zv6i~=%8&(c1~YVyjYA0Ge1>+_TKgSq?e(gvx&StaXf&%`@W$?`JkNhRn^a&bC-zdgEURwOOj;0rSJ^k(O@um)V$bxYzh_qq7_mbdBzEo#2qBu5_#qK}as&+$4B+#@VDJ?J_H$esEtpG-jCYn+0jy5bA|s{T zlu{m5i_G@km;3H@-z4=EPF6o~8)(#>A&+U(z7{{rvZ~kX0YHi)xK(n@FaRD527@p6 z`{Sg2J_N9ZKo9YRfKkiwnCdq;@XXJ$%=LOb4PQdS>C*&8y?~pO+UF-aJ>EksQ5#Ez z(?bHIUiHnvU@);Lws|t&hEw7<>@A@ZgY9MxlMlZFHi4>DX!0t<7>G-|33wNI0;0CD)|5a002ov JPDHLkV1kGQAYlLi literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_radio_on_disabled_focused_holo_light.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_radio_on_disabled_focused_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..d552930db44aa4b93caf3aa3fcd3a130c3398acb GIT binary patch literal 3401 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 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_radio_on_disabled_holo_light.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_radio_on_disabled_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..5741490d36c0979777650f773b326aa4db9e1b86 GIT binary patch literal 1643 zcmV-x29)`UP)s_yF z(*09!(sYxwRD}zX0Jq7ZoZ!};Do`(6kU#`gReL}}po*%hl!`Maq)L^5Udn|YNX4oG z(gT0m5VpxGiT7W+-QBU*9?y(t-g_ME-87Kc?ry>bf9GJ0#xuW=vLB@T95)gnV08w*e*M1+g#e*jPgn;a#IIHZtBK=%+lw(v<7?;k)_Qec`g8lSps z%Fyb7a&$)LnIMN`lXV)4`aS?u!DWtVlvquB)2*MY2S0Q520ylLjzyDZ4#^rTgv0tU zBtTU##tFv6Mtc2g?H9ExZoAt61m;!wr))H1j?m;|T8rkM0aOK(oIs?mKE3^tM)Z!` z0ZjLJMgCzbK8|4tZx{q?DD(3?ig7PL?|ly>5{ZSx!s?yLeIW6qq5ZGjh)7>@hT-!~8lB*IwNt-qx%FS*+ujr1^0Jxwt@}p#oyz*LU|vs711n>kmf@>+J zX;i}6VE`&NM{(RMzwC71^^$YGc=2T6!EgG*IKp-wU*UC;gu{+#(U>iClcDj76e9`I73$Gi|AWi1r0BLO5qB~oN_j2Ks z$acitS6xLcL^hmyFSj>O$`&^1g9D_nMF3p~)?AercK=0RA#9ajb9*x*BEZ5vIDkZ0 zNe_U)HjbA4=M{sN#tH16Ygh@ALyUntmiRnZ2d_r`0W?P0drxu`(J=#C4WmWJP4;T* z6q>&G^aqGh#_a)^nLsvGG6+B=vNaQ8x&S7FB0e}kM6Abpzs8*L-R-13=p7_&ck8KE zuR}nOk#LBaFeEaC7ev>AuXK)X#+$mY2Rey2k8YmsbRBq6m~kTdF3EiX78M@34|9>h zpy$=I+u4<1=kDXc=wQcPd1hx`p)ry9dyX7l|8{-QzhDbHES?t_j8NZt?#wHBt0|of zA0yz>Vle3G!vH{@FM@mdJG;-(%LH;i8^B&B(B02B`PF#vD29({4$enIixp{*4Zoa?NW3C!xFp0#Ixa}S)K*P#Le{D-J{uS^8Ic!Yw-I?}p52qWvY^~>_qM5~JjTM4>98P->d+}V+rY?77)Qjh7JGoeqZAG zz7KmF6lv`J4GK*{#Lj)+p!f{P-WH7wB!}6e8EK@EMjC0Pk^T+!A(8RWOu6^~001R) zMObuXVRU6WV{&C-bY%cCFflPLF)%GMI8-q+Ix{pnH8d?SH##sd==8PG0000bbVXQn zWMOn=I&E)cX=ZrXPw literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_radio_on_focused_holo_light.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_radio_on_focused_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..4a74cb9730c6f329bd69a76d7005bc3ac83376ad GIT binary patch literal 3535 zcmV;=4KVVFP)qtERZyWkdQTDL2-y-s$dV9`CHe9G_3prr2KnOSvd*QQ#oZX=oubRXk4#0P?FZ0UiVD z5JF{v3qTjphfw2Kf$u9rm`^>h0;ocOnMIh!&wxHd3+X%M{f+?`YsCtz10DvP^70UH z6gUh-bhUlyspYneW^@l{6lW}Lwv3FLEE&zH0V%6#R5cs5Xd+_{d%a#&zFR?<&|2U= zgoc37pbvmUcM8HC0Wj8z9oUEv-iq+tM}dzJ_Nx8kSL^L(hV6?3SBh68v&t6`0@)C@ zZjrA_%auf^`Z3@L;P-mHsszFfJOnh!=Tis}J~FhBx?LG=8vyy`9|L|zG86~i2M!xr zNbY%|#?n6IYCfB=uLT}JsIMRBL6}4ou9-v`0+@P)<8&ib{vfaq==FNNSqb=B;7dRW zIS}^Ut|r_T0Mh2y0Z$>md>Cj$IH>G5pI%mTGUj+9o-uC#RN%jXt|w~-gRd>`Lq20+ z{OOPd(gz01jsMbHJ1>|jUIIKR?|UD3&+GMur8caRzm+3Ec$eEWhua095%>zi=HtMR zflt@Bd9;Sv@rL7?z=fw|YbxC%E} zb&}!#Mc90{VRpPh3ZDgbwr}ws-%u5)M5t+rTxEI7Ibr4hrr&Qbx2A3Xv9aUys`AUb zB)=~LwK6jN2;p==e2`{oA_dbeX~0-33xFL`sPn)cLkq!gKE15wR~MZxNv3zUZ}A2Y zN0yab%x_aQv+cWsk6I30D5^{(5+&($x){J>v0QaH9Pzc)BSSA%{W5JeX7t=3h43LM zK>b$7(wa}guCF6hey8*aPTn{ySOMCjn+CGj(HA;!&y{RWCa)*8Br67L^P2|1f5Rjz_PkI zXLc_5Q5i`DUuMuI}l6!Q1S;s1|JkIuUuoA#$eK zApagJL?}}sH_6Pd0dWS8A#AT#b1tzTy)^sFQrKO8*W7DDn1M|S?|9+JhWbz_qDU-&t^}PbKAo!){bT~Eb9ED&%M0r z(g)Z6-@WXg`Nq@tul>(b>;L@Rd(5+=VY9g*AXE`*OXe&i0ybyFC~TE?>BC;fk%ErR$u5 zy)^c`Jn2S_Jul-7>}9KG>AL#*`V|*0Txfcw|5;tDEP$oNlGRq0$4*ND*8mt=h$Avi z5ZR>ddhee|g#u8C03nNr@l${O`%6l)W5(rx|BdF8!8``7s%Eww>yO@Rx7!>3c8#BU z-_CJgc`~ZH^a1t0oxHy4EDncbX^-#XB2_cn5Za{x%GR18zpH2@3^aK>9zCZxB6%z& zmpI*&3I#wfYGe`lJpQDu0-(O^zJu zIqF9a*SOv8nrJjy`=)=Z9zI(DWu8q<-!tp#1<6CXd~87SsN`L{DHRHUlN=nZE~MtSlZ0N*?qyJjOHu zU?W%LNCI5Vnsk*6lPhBx0VR=0lq!nie0A>L#Rb0qhV<&(y^9q^aV8RpQpShS#){&R znB+mP9I1#HMK8ooN`(TT+n8R{Bbin7=aUl|%VaWEqtR$BF{KK!!<$iwDOF)K8m*a3 z#+ttefU792?`;8)!Oye+kbnQJKJyz*g$k3OXY1OaS9a~>vN;XV%x1HyX_|U5TN=1i z2IHf)u%>CM*=*L;`&uBCZDW5+hd73&I6$(H~zEnZlTajs0PgE)xU*^s_9FE~k zCKLS!|C0rUqc@{B&u{I|WHM2^-99{FA5A)9Jz3K86&-+r%4CHC5GChbodBigOblWF z3&_>_vp{xPrD;ghv_K%L))x5w8Elw^Y`tL4&&rJ7tj4Me~;EdUIW^Y-qsR&2Z9>q-Kj0X2Wwxl}iUETCGA znN-sP=P6ZHkAJUwZY|taTTA<1_uN`lRZlFdn{!6i2vmKaj|W}#=dTnyfn#2;SGV^i zlE)-6UYU#v1t5tifPiRH=*Qa;z&?Lieq}%QOEEEcLZR>a@!mWXz zZR>a@5(Pk_ze?p|z{_vub4xqJ2gzUt~~ug~W@{P@S41BIAFTWn?fmiEU3KA-P!b#=9O z_v-z9sw{<)ddFkwPU4e9YpZ@+py1ehDv(I(?Ugx&R zTVAU@6biMw-EQxmH3#}!N@ASw!)b-%mrFLCP1phU>s=(^QTcBO@J~lYD2!-olmqu5 z3XuEPw{aZNoY?Xw2b%(WJV$#=EZJ-7=TZ{dyK8@X`j!4?hkAS$`+YuNt>15QZ|_;G z8I4O$0BD+~W-`68(b3Uvm&-MtuU%;X4|&|Pgsr(QkhI9#}!5Gc6+?3ilJ}0 z-@9Q^^qoHUiUWglo<9?}_meA%8i79t7D>T(8(J8cE&wcucGzZl=^&y2ne=!(^}uU@ z_Tb!0Z@;zbL=d52`gDke@f{&OXo2xf5#5aP%_;j%pS$r;*!BGJ%Oz3ZJ6^B1U-I-2 zu${a@?XAoZ+)`XGTTh1(Ue*Y#1lGt1tRK;|FdhuKcD#6~ap`bc(X+?6T(P_rov`wM z)9+6Xrb{DO-FlX=^w+?0C?Z zn3X24IV$(h_eFQVIVzc$*d9+IniB1X7UCX{M?q9)9|gV$q+LZLksA9|UsbU>FwdsO zoJHxF$uMTTG-4^goVGZlBjyF+t3?Yhq>9U;BNmO%x5tf$BIx!Bd;apSw#V~8m^dJz zB6@ZEkpmJQkH?0n(yjrPa~lH^a<39ZXX-w3!x$eR08ieo40m-z#^do=5n;5G8_x-b z^MV^4Fq>KBOS%&&h)Dxc)+ z!2O7?rH{#Jh@))~5x0ls(jz>OeME%Nm58{0E;)_T$K-wm95J*M9_dX+cY9EBTtS2D z4@xQ%?hhf7n@%7&6<v002ov JPDHLkV1l_dtCs)( literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_radio_on_holo_light.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_radio_on_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..43b37694033c7eb2df6c823c3d305d93f39bdc81 GIT binary patch literal 2432 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 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_off_disabled_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_off_disabled_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..61230cbdc9957f51f5da42d2b0b2d38462f94934 GIT binary patch literal 528 zcmeAS@N?(olHy`uVBq!ia0vp^WP049U3n_V#(N!wv#$AM$r= z26^l4{qksOM}mJ-{sZN${Fm$|oxdoGX=PxM)YA;?u=rx_gj=5pnT>$A^4?>>KgGNtHzm*6qeZ-SxEZC*tMZ#`ex zBH-gvb30T(P+uu5{AkJ6sI`wRh=gWOl{rkKgTuW z{qufC4(kQ7O|q%0pZjqF-S+1E!&6(_!}lJ&zU$&zpMCk$Mb?Qs2u~;l#vOyFtDnm{ Hr-UW|A0gbC literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_off_focused_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_off_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..926f2f336b640121daef8099f29fb88e2b984c95 GIT binary patch literal 450 zcmeAS@N?(olHy`uVBq!ia0vp^W(978f1-_G{+IvgO-uwUHi zn%sd_@AnHt4hoC#U(|lWVcQhvX?=iKKt9~LNH zPTA)-t7T8;V~aoMHc4CltAB7%Jz4#txY{ya;md8Ena(8-KYOPgklDdxwBUu|a%YtT zViinsG3SaE?kzvWd!J$Y)#q;uca|RF@lezdTQ?(q5~IG)^9{NU+|#fBJFv*Qzvrcg z<4kWcu1PXo?Md&XmS65WdalUey=Lg5U>_f8m@S8oY)MSIWnh>)V}=<}nDh7KoIrun zql;9sHfpG^Tf8n}3Pb{|-fc_LrLvANop=AAt(n_=`{%tmMbBNL4S~*GV*H$?LPlY_ zTFmY_zGtfn{XbmrO)Tvwn%E(FV3lJ{{GWH#tqy)gyYw5vPyJgO{ILAj0tUNPF-=L6 zp37<$@XZNTPW*97^lHWWW74KYaW~fg@QwVZpKy3bQuc$%nSYFVdQ&MBb@0Bn=WlK=n! literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_off_normal_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_off_normal_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..54e5e8eb7e10c2affda04d69f491440015cc82c8 GIT binary patch literal 574 zcmeAS@N?(olHy`uVBq!ia0vp^WbMv^@mxfno3NBvla*1PoEYp{!1!1zGOn3h0u&{W} z=R3bpA%JPR>%UwbH@koTJ9ttm*IFFnQ0_4}^snmS?HsfBuXgRzKK;p-VP^5U=6g#7 zm>fRctIwMC>~Yb~J9DE?g-ri!^*nFwR`aOW?E+3dnoAv*Pn{B^8G7dVXX)p6I1fBe zzvoxiskfnM|NZNsp|%#!56!roZ8TG3Z7J^pmIrZB$%iC@EuKHRQTljJ_qC{-OIxfC z?)h$X>|$((;i2mL(mikI89-bEb`qFaCpB%R;USLS%MA|p2)aomwm^t)J}VUUHpQ)f zUoW>lY&HKGiNrM<C+-gz5D2++a*@KL8lzeJC{y*^0FkT{qVzH!)M$LSJ-vr`j6kuF`J#eR@^nv zfx}Qk=ebUDPsXfgtGAZ1KgjVc`Frmp*8!dbuLY(3m@Y7FV37O&L!sf0sMd?BENZyZ(JwZ;A8C$aml`}1PoeYbYIzRBr}(X1|DOfz`8`njxg HN@xNA+%W(Q literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_off_pressed_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_off_pressed_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..a096c03cabf639c151231176e591f29c815d1631 GIT binary patch literal 546 zcmeAS@N?(olHy`uVBq!ia0vp^WhNBev=V{@zdnf8o1U*e`Q-0K8hNfPO3Tlm zKb_RmXvlVM=k8tW?DfUE^<{&fXgxdPdwEatv)Om^zF)0Af94O*!(+}a61{FAp;ym7 zET~|)pxP#}F==mHcxq(c+O2=3<_1hlO{Hk~o z+r*B5`%k~C&yZAd>gce(y>11}J#dG?8IC_{C+c!yI^d|0`Z*r_J7&bF@nHsN%N{mrmL5;Rj}Xj=P(-`R4M?YV#D7 zI_~BRg?8Pm=2GN|+os6!>7M&h!O4dAa?JW4fBbQ_#45L=qw`m3N}7T3+wZ@p=iaVk zzA$r}#KsMG^WLxBs=sFI`u?EW`^HDkWZeE%9ee#dgVXhv8?%=&6!1O}ZF{|h=K=Rqo V&Cd2^lL5v(gQu&X%Q~loCIGlJ0u=xN literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_on_disabled_focused_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_on_disabled_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..8da24f85e0eced4b4e964bd67bdd175ecdb0e2b8 GIT binary patch literal 445 zcmeAS@N?(olHy`uVBq!ia0vp^W-}bzO$n$J470e=b?9 z%eC^zI=1IojnBSBuf4&xgH6u;_k7FKu8K!Z%}gsU-gnX1Ru8crk@7vb5 zf2LR$$sG~f@z`lql6yp1?Sr!m{$C2xf2A&(-7uTAam`H0K+~yd4`ZtioG|&a@r+XC zy}--qr}OLLy=soxv)%6Be1P+Q_?s8*tCDveTDpYM?&R-tDRytXIeURF?-E_TV7-!Y k?=-vn@n4>XUUOd|l(Y0GFj^QqUHx3vIVCg!0OY&L*Z=?k literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_on_disabled_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_on_disabled_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..56a4b723b7d8853315267a52211c2bcc42103da1 GIT binary patch literal 633 zcmV-<0*3vGP)dJk_oK{wx{=#rykG>QVA9K?%<8j2*Fe6K?s zz;NM+w!*k%_yK|-iIUzYl8*x^f%D*lV{zf~?*j%{UOEV0B(cy)Vxf`5Lc?j`n@*?W zZnyhcYkdu%#e6t^`b7u<09#|s@1iK~CX>mm<`&kD&;CM0AB{1;nAu!HObDTgXaqn5 zs1t*WSi7pKYXGxeuXool#zquHfvT#;0NMaHFO_xaW0Hsz5t&zst^q`3h)BU9Zl~nI z%y{eO*EEi3vtkkpjU*NtNh~yySZE}%&`4sTk;FnHiG@ZI3k`{XC)uJP>KcoZsKuh3 zxQ@EUq9lohMiL8+Bo-Rcin&g%pM?=hS|L%$j>f-1qzaQJ&{eN}1hiw6oX%cs%hQW`?y^=XpL_uh;hg zYQKt$n3>P#v*B>~X)qX!l~SsS*g5RP>}s`oST2_{0JY87B|-_{!`}9M?k#z)p~t=L zXsxm1dPM?I?)H~n0axOwHTzAe|10TFi4aL*p%JZ^0G_f)O`)bE@~i&n<`VB;`E}^f T>^$gW00000NkvXXu0mjfyMZz% literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_on_focused_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_on_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..2e27191fd649e6414abb0991d9757b112b158f47 GIT binary patch literal 520 zcmV+j0{8uiP)-W5lu(7r+O0Ejilm zgVV=iNDN+Ne^vXe(_u?5{b zd8+2KX!IQp?R^)&kvGSTWhWZojDgIGhJM*~fmeJN+e9j;UP!P8cv_=;SA91m-4N^} z<=TDFq&LW;do8I5w>Vs|}@qPkWK3jyuzpjq}0000< KMNUMnLSTYZ?&T%` literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_on_normal_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_btn_toggle_on_normal_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..17d117e269afd7ce2787d0af161e992547fc4702 GIT binary patch literal 563 zcmV-30?hr1P)%}6o%iNb_z2c3ocz<7*~RzYq#@Xyua!CUuG-JQn}k15WLJ%X`2kB`Ea4aRl&yj zjWIWU-#<9#TnOQ!Vnjq`S(fwp z{Ki^q0E~$tMXcIvHcegE57t_De3*w*Rn^zN?;8LW0QXvY65`HwRunOAh!{6Sj2j}x4H4sph;c*2xFKTP5HW5L{#<0s zf~QO4?RAb6}PtLiM6|kV|x=L$(wuvSct##RTU6s-{ zCD^cDuTj_ai^XEGn@*>Wh%QBe_{ ze*X60;NXK&>K!wKdHMd?EJ6qfAz+O8lIQv7)6>(hE55?JE+?MP=kJN===S#Zo3)mR z=oz}}j+7En6vZ<{LrGH-eaN1To(TV!jc?d?SeY zMiBFjAm$qq4>#FbL90ism9$!{l^fTs9f5MNy-(rRn5B%=}=wZKATL z(W_>Rp|UK^vRqLV#u!sK*|w9|eSHOlaR2MZ%$Q6jg|+r(I2?XTl0-2x%CfAkuCB&e zmOWZo^S5)l)|)tqS!?Ca&W=<{t(C>9sv>5t(lj0aO83@Un$2eBDH*Y@YiX@@3Lh6S zO4GF3-`{_8cz8IMQj(PGh3573_42e^D+GuLL?mak*^9+u@g2a*i{jEiJsOQJwbpxD z>(}>Eo31~SM1;j+ag*oyC4iOj=^8W&KnFk<+xrBKb7-C-^-(Y++(;-kJI8}>`QPwEn9sQH-ZdZGe%o&{L?Tz_!>u1Rjo7aSx_Qg~hR8*syBF$%_wNssuVR#0(s}V?1TcIUJYD@<);T3K0RU2? BkL3UW literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_fastscroll_thumb_pressed_holo.png b/flock/src/main/res/drawable-xhdpi/flocktheme_fastscroll_thumb_pressed_holo.png new file mode 100755 index 0000000000000000000000000000000000000000..75c07f2f9d4e6b42f784f4d403e9725d7f2bbc93 GIT binary patch literal 605 zcmeAS@N?(olHy`uVBq!ia0vp^Hb9)i!3HGFiY~S?Ffgfjx;TbZ%y~O!uNSkUh+DrR z%jva8oYt&85_CjdApXqvjUQNDf}%T?c6e;?k~}1&oA#&mY~CGf!TmMt(^OM-exEn< zX6&{do4-~abTj<%n(10^>(z}Lti-~OA9@uLAV zd=*ntt$^iW_9aZp?1c<6jQR&+Lk_I42u;{CKZM2CFQoDJ5uL|sZU^FA7H~XNd>Wsh zSgFKy_pH0{@dJ_tn^fX??=XZPsPLR8`QgiMBVoZqxyto^lVZ2cYCDi=XU^Gvrr^k= zyxrxuQ}$mgx?>!^TxDsbs{6+(vv-dVBD|rqrVrC^#0btU>auNSJ6aw7t|ENypt-R~4y=P4~RCCTf8R##5RKIVCi&X3rN81D#K%((dep=M@t z)4lcmGLKsyObuQ%_3ym%z20q0&b?b#nRRLMj%V_@F-$8jw9C{_JD+>@obf(x@r7T~ pSWO-MuWS7IxB%Ugn+p%+cd`W8?EIzj1(<9YJYD@<);T3K0RR_H5<~z1 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_ic_navigation_drawer.png b/flock/src/main/res/drawable-xhdpi/flocktheme_ic_navigation_drawer.png new file mode 100755 index 0000000000000000000000000000000000000000..ac6c08c3fd45e74d35eb0b792f1292226c74cb93 GIT binary patch literal 133 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzA5Ry@kcif|R}XSB7;vy0G+y$f zBsWWdY4-77VzMqfUI}jz+x?IEfZwJ5^lM8b_ct)eY%s2wb-gv-0mw`c<2b;W*2utU faX?3)fm!e9b(YH)LyWtD<}!G?`njxgN@xNAA~q{M literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_list_activated_holo.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_list_activated_holo.9.png new file mode 100755 index 0000000000000000000000000000000000000000..7aa657254f9d85eca0932b7c365168e2aee00ffe GIT binary patch literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1|;P@bT0xa2TvEr5RHk+DF^ueE&eEZQPjZ5 zz`)?o`vi;b!whR0HZ~pX=oH~Sk&s}rh@Ita?vnj(Jtvm#?EP|;5_2(9Cu&3~=|WEq2}tDnm{r-UW|L`N|T literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_list_longpressed_holo.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_list_longpressed_holo.9.png new file mode 100755 index 0000000000000000000000000000000000000000..22ab0790620759f0a2e183ee1ac40c09c7796b7e GIT binary patch literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1|;P@bT0xa2TvEr5RHk+DF^ueE&eEZQPjZ5 zz`)?g@87>47%*tx*ira6O)bbop}Fy5hn&Ec?Gyh=87fVGW_BrD`Aq$iW2(vwfyL~S UCH3cgfo3syy85}Sb4q9e03E<5tN;K2 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_list_pressed_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_list_pressed_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..5411638532f07085cfd9cfac5598aef0a2747465 GIT binary patch literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1|;P@bT0xa2TvEr5RHk+DF^ueE&eEZQPjZ5 zz`)?l9Lvib?TitQRaL(t)q+eEnj0^6$O&xOKJlNFq0;ncW|zX1&(tqDrmDm T#&P;Hpjix_u6{1-oD!M=2qh%>1r1WJE1Ls*cJ{|veUO|(2+uXou@pObhHwBu4M$1`kk47*5n0T@ zz}*SLjOHg#uLTN*c)B=-SoFS~V#syCfQQ+CVSeVn|8w@Z#opR_O@N~@<73e=BaV}1 zlg{jC32zopr0R9F$z5oCK literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_progress_primary_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_progress_primary_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..ddc287c9fcbca22b8eec28230caf0c883703b693 GIT binary patch literal 461 zcmV;;0W$uHP)ToQ5JsOP)Kw4Eqp|cdmuuB~w2O9w5K5s7JYksF#Gok15}#y^e~@_|>=`GHQ3G>1 z3Xp{^LBnZpf~`8h59R6D<4;$$$4iH^4-WIH0Fxto{k*H^ZFo?knS%&`hLUk|t*8&> zA~q~ySPecx3ZQ^xft*w!QP*wpau2itz6OuRPHtZ+eU-9yT$f5&B5eY28_EyR zx|-iLdpyTmDd5W3RKK;ony+H?_~e=4%)0^9mas?fd3$R+HlKO_wcY?9b}&abr<>^j zt!>+W`;OSyEAR%-jpHd$7W!j)fr1lIiQp|f23#MgHP5B$OX`46$jF>3D=tNcDq>?{ zFDHI$D{2@${S7p(=IicCUF~xY&Qf%cdMdKmwk?t|+h99bc~>zRR9mUvi_c4OJvL4N z7mqirN%^-6kcqwO^>z`t3rZ~jCa+(;z4?;0MbkOa@{s;=BH2n8bG3aA_>yj~#2)qa zdWhb~OZ-a(IDT{PprXNx+~`Gnyhs%bE(xf#eeV7Nl^lfc=E{Hx00000NkvXXu0mjf DwI0wA literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_progress_secondary_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_progress_secondary_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..cf8bf3a7c9df90302f16d818ea175433d584f35a GIT binary patch literal 146 zcmeAS@N?(olHy`uVBq!ia0vp^Ahr?*8<0#p>+uXoMS8k8hFJ8zy>gKAfC5kJL;FjS zR*RJ_lBajFzcsCFZQ|JHtv7#$Lx>tM4$zG2GjsoKou4@o@S2 t@`m?U`hO{$zIj|mYkPWXzgc#bhqbFLS4x?}mQtW444$rjF6*2UngG)7GI;<1 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_progressbar_indeterminate_holo1.png b/flock/src/main/res/drawable-xhdpi/flocktheme_progressbar_indeterminate_holo1.png new file mode 100755 index 0000000000000000000000000000000000000000..e1288d9e79954b296e5b993bb4fe575f3011f2d0 GIT binary patch literal 813 zcmeAS@N?(olHy`uVBq!ia0y~yVA%s?D{!y@NePogK?Vk zL0sACB-5|krPK5l{Cvl^yHlWL;%~*yzEo*piE0z$=M4b?J;y)FzB}&R7w`9;5oiv> zS&J!kV8(>Wjp85yU&9YfKx%;|TO5dMa`XTaQ2|nRL4;W!Bs|MV$Gzp@_07KK z=T;x?H9m0fg>l2KmxT)EdoOLj^wVGP^M=LuqHUM)M` zc;}1PeXr;B+x`g%<`-5jpYHojBJ;)ZrxK5gUR8GdKYZf)1pW>7zjzkpRmq(2UH@fm z$-kGsl69EZmo{GCF=ySx>r-Ts&%FOE^4ZPf{%2EWpt(zt&7HrlzN6!@fZ+Z~NAGz3 zu(%!lyYKYR@-wp^8lO|2cRz{mZ~6)L8{)NW0p_-RPnL!M3bXp3{kmU&`FD%julkCs z%x~J?8Yg}r{3XI`lYaIuTlRGCv8QjV+n-oD=zfuOxckLo!tA)0 z(HH-;|K@*qaNEKB^^8}X<}+UL?PuQka_TSsj;T|ZPsm5I z_2r-SYwh>Vx%c?@rzf}n&+)H&xwbm{!~D~hcUiI@IG>5Xs)?>%|C;=xPv4BB-xOZ@ z{!`-l!F>C}v;1tIGQa7yV_q>^j=kuzcCGf@`d>E5EIJ zOR+@bggqS6uXipx`}5lS>+^x;Ohq>5zldRd*r$r;jZ+Hqn;DS9#a6XGI0T%Ok{+-E lX@_7QQ1WKT=)#{4|1fXtW?=p^uR(%2|6rRacS!6)t*#)k&X-3@t+r=MrMJDGyk?1? z)NZTG`zHrm@2lL!4>XpcBzfx>J`m%8Wo88Z_qN`0D zM8{tIzyzcgXdkZgG}#}u)wakwN_Hn}#?(8^8I%98Wj%4ZuNQhhcT;xR+ODUz`X^F- z*-xZB+xTR|x>r0~-eec8KeO|teaREK14d_}8>K8SZr`-;_ROz~zhC>FVC_7~zp`@4 z^n*orPdRS7ssH-#E+dJs%Dk+5ZgU&UPTDq@oy=e0=J)08)?aGv|98~+*Ped6W^;Z; zuYSqw>9Hp3E^mvtcJu1$7;~xYmFFB@-=Uff>#Dtg~Dt&?dU zpDf$+1!ROO?k;O*%KNr`zv@rx_U5#yPxN*>ek#5qnQ>Wj=l9%6^Oe3cOiQ`Pb>&3o zKH1gp^*!Z^_We4Sn_m05Wd8EvzbZPfj;qwG{hV}|@w4V`$Ir)|ezSfE?0b68+;gA* zr{6%{f!9zk%1`<;wSVpVsulL_Zas4^D4u_A*}weD9lfo=4DuOz-;X)Ze?RS(Cc`SI z@{N5@1*6l~uRr|v*RzX1#M|8zR;lDGiT|d70Y8n8F31Ezb-+#8QhT(#8 zB>M%k&tagjNw(f8`+ok`eRhXe%lF@meN?>X)xnHCZ`2*~pB3eAj{m;niXUS@D#TN3 zzn|W6u=Lt5KF_+|)0Nwv+(@;R@B00;LGu2emBtrV1sA%(NDZNbo(>Be0 z^L{hz_{h7!ih1p?S$Afy^9P!K32ORF&GySy)6Yv@pC5F;`7O`UV>}bgeCvO1cbAiY zzuNf1uIokl>prS}XIR%3ha}U8Fp-NE@We)78&qol`;+ E00e{GMF0Q* literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_progressbar_indeterminate_holo3.png b/flock/src/main/res/drawable-xhdpi/flocktheme_progressbar_indeterminate_holo3.png new file mode 100755 index 0000000000000000000000000000000000000000..f2c2025fcbf429bd25e9b99bf1253037bce08eff GIT binary patch literal 1181 zcmeAS@N?(olHy`uVBq!ia0y~yVA%s?D{!y@NePogL7+g2r;B4q#hkZy_x3#wkzjjJ zzBprxlSqh&NerXtbyv^E9Sqx?new{57c5w~$YJ%>ER!xHk;$7aEtAue`saSEnD5>- z=Scm{$M0*ZpG~g6cV|C0&}fD=$DS>%2Qe6YJP(3I8bSjA5u8vK(zwW~3 z!@e>O(^kGU*xgrI`ez$!!bcv1UhhwZNi~cgVm|4fS^qBmmD=OjTYV=prN3=`eDd2i z&wVE{AFjT4Q}1M~ocdYct>=8@=4(ECu#u&7`OT*$^Ire|an$(0o+rr-uP5Xu);<3f zxhSt_<;J%W%TnW_k9*xbXLh_zUwcVZ!sO(lHT-TjGj`YAznSzw=-buReb2X5-I>~P z#P~o-+mqzS+zlV0Zr)cW6J5V*`*zPwx7B`HF0cPEowdCErGER|`_Hx<`u1(ZOa*_w$CYBpg*~0 z@{{x8zhW8Y&)!#)Q^RPW`%~=M_t;7Gi|5;3-}&pZ@1*^oe!MzjSAKi>^pn1Fd^d9a zR!=ZzE@SZy)H-|{Xu|BnHVo?)Ys*W=Gx+%LYyCO(-<7zo<@0%q*Znj3@&2nfS6prR zq4Kku>*xMpjSvHd04NNjO4ly2JM3*Q{p`-udCj*24>JQ}eS_BYIS;qy$Gt885OsBG zIYZpMkE{nuPi!|>TWhTQ{_Ez%Tf2gbeia;b)YGiED;xK=?{u>AL(`pSo!?Ab?(pQp zJ{wM^hSx7n9q$Bcy8;TD@ViTD*3XZ97xOoBL++&eMZYS}Y&tkeVE3fQ%a7ZXt@*#x zK5laAi|R8kf4{1gy}bLd4KSgsHT-$w2UCItFchv|KX12W_k*SLOJ8RnFaEmRcanWk z4Tt;nzZYKt^Tp2k>FbMYykghBdGXC)ZFv;m6*KrWcyVVu z(9FGxU^8uiW-g!ayLi3q@oV$ta`R_DX1rOd6Y+k<+K_AMl~+H@SWH{gcRKbQP***M zuH+o+ZAUk3>zjJG=y&d<{+D@g%G7tK#jScjchd2ox3gca`*r77;odnFY%{K@f^2*6 z6BGi~p7VB{w3xo_u^oSI*sYCs>UXd1`g=Szopr0EZt<%m4rY literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_progressbar_indeterminate_holo4.png b/flock/src/main/res/drawable-xhdpi/flocktheme_progressbar_indeterminate_holo4.png new file mode 100755 index 0000000000000000000000000000000000000000..544620e5659e2a5cdfde56e6ad04ae9836cf3fef GIT binary patch literal 1129 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 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_progressbar_indeterminate_holo7.png b/flock/src/main/res/drawable-xhdpi/flocktheme_progressbar_indeterminate_holo7.png new file mode 100755 index 0000000000000000000000000000000000000000..a3e2fd9753c54db55546fb375fa02ea3a4c3e4da GIT binary patch literal 1063 zcmeAS@N?(olHy`uVBq!ia0y~yVA%s?D{!y@NePogK?Vlqcb+beAr*7p-Zjj7>?Xl> z;XG5KPLs&Wpd~kjmz>&J)S$rLv76!K&Y}*Jl|d}sM-K_GShkuNxK56%Qa)z<_&Y^wJ-BOgU$JK#4qy;0Cmj0_M|$gj^m1Z-NvP1uhlodm77#Eb$NKz z*H3B^^WQw&^X9XON?E1Ozq`xs|6{mTt!IBsnENN=1+kxeTf87zy5GvV@$NZ$`Blv8 zzQw8YFIP@AkeHur`TX8*=7@Yv`(^XX&am$L!?5DI_ousm1XgtZN%CIXxBlp^^Anyt zkNJ1_+pXz~Vo!gWwC!-^UvIy;1@=~H)1Sop>0k1Hx!Jzq>$FdgzpMOzV)Vtku4eL+ z<0tqVX4QJ`n=Q{|Iw3wMGqdLJtS8Q|WaH0YtId;p_vwSZ=@PN`#ad;xryi%yf4X|^ z`$>O(dc9?O_1$^W{z)}wQXkaMeX?7neoxSDh9CLUzz&)VbkKeEpVn`C*MIkX*M45^ z+Wgyd;#T~Q36sx1&wXIhC(mOOPB;C0TPe?wJL`1$XXgd1pSpFY7MlF^Tyg98_1ts& zAAY^|M(uQo^Z}_)Obha#wL7eSB7WUy-AteNLGhn|+)}-H^^OB5zA83uuV}qqxXEWb z;~K+%EzAx7X2>(GofIE)x^RVr25oO1C8I`Uv7s4+Tj-;Eu8tD z*M4GNkQYDcJ%zO!=2>Ft$!PDgOt+kF#y?EThj;p=~U z6wGQB$v5`m{a7uHSLGeMr=Yc(X`C&uTjy!vr##COS0TcWOCmQJXNYmX7z zwM_KX$I!UF-`F?Y^ZxXBLa-k&u71q~dl0Pt#yiE6%RLuYN&mU*edn00q3yDq_d9`( zU1oo&Lc(_bvtvIjeliAZH~pChOpYG##Xs$8LCReq%EjNx6-BPBK5h5CiAiUoJVWiI_n^3l&PuAe8q~@RjDm*M0jOEX p^&mG$c%4QCBaprzikAQWv0hE$mCiqrYzCBN@O1TaS?83{1ON#F0dD{R literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_progressbar_indeterminate_holo8.png b/flock/src/main/res/drawable-xhdpi/flocktheme_progressbar_indeterminate_holo8.png new file mode 100755 index 0000000000000000000000000000000000000000..066d6718ae2d0f6fd55db5253d02239a4f63024c GIT binary patch literal 1049 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 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_scrubber_control_normal_holo.png b/flock/src/main/res/drawable-xhdpi/flocktheme_scrubber_control_normal_holo.png new file mode 100755 index 0000000000000000000000000000000000000000..35c1a5673240eca44f902c6ef072e9e20b036ccd GIT binary patch literal 1523 zcmV% zZ)@X55XOHs#`s*5CZ>eDG`%)yTFM=L(c>tT`-1&ueG?AaLNKKyX(&kuC1;FLj4{5x zn4RoMkz~nQD{}LIu#l|XnP+EaXa9Zj@fV&n+BMhLa#tP}?y3drBtR8yyLveZC;Dg@hu&wyY}K1Ga#HxwhT& zXLyppD|ZJ(zzDcC*VsK*Qmmy^LRA8+;$5JRD7_231onU(_dKtzUdppMFabUQ|G4W0 zA!CBKj7mgQA;2o$Lt1zrI0Oz@u5=OEw%zq5qVySXijZ-Ow+baPMG3Gi{|<41ra@f-d@!6)kpS)Hw(%|BMCBFR zh>SDD66W}BG8K6QXr{J>BsFhoj@jp70Q`jS8gtXd20@9SSt^X29I_mD{L!g7mzK>7KALyG!fNlLv@cS$b zkW6(O(P=3Gw)OYfJkw7?T_Mv7(g@JVf5>_UHg%(q=(2_Awv+(P_zvojt|lBHI`v5C zYaq1 zgg_r@y(*bx%asu-Ch4rvJ)ueAVlOK~`}(Go11b6?gCNO2gskk5%h9qRqZd%rwlv)Y zSOOM_->T4An5LAYWr5K#X#pWC)7-u(%!) zL1%~zam$oBci*f^URIg6_!VRz-oL5~=z9BBpo$}8R5w8;S<{BYR)z>sd`Q^gaD?cl zizPrd0UroQfL8uH63`Cu(^GarBy#Uh5wj0DAp)XPAp*w0C9>iga#m=L?C7{e$W0?) z%z6?rt$R!X0;b4jgV%($0$b0Bu)%g2B7^|lWtN!;sSjQw`*8j*I74)LusoJVfV#mc z;S!L?#01ghf%!u^0lGi$0{A=m!G>Xh=x~8wDb6K8KhlKggnE57G^`G>{$G(rfVF}@ za8t+|h8ZGzX6r+KLZVhMM1sgBxxxaGc}Py{Lp}j&1=qlP;3~M7>%$cyvu=>c+%1-u z=-3|N97$Y0rP-ULK(f7KMCR1UZ?SB~){YQ|z^^nrD2@;rt?B2c)J0NVO^!e!Dl~Ju z+$~;t9nb~YDXQ^62P=AdqX}DOLFBu_ZK2n6^xNcTjzFpm1TtV^C#4_U0y5J9KjNDgDhlz#rpRZO_22V1W9sgJusoL zN7|-Ns7quU$|0VJP_CgwpCP8MX9JRFhf1T0fHp4J35^TZj z6sB@(4P(SC&k1{MPPe-@>e!qjjff7S$PG{6;H1I|H%AnVctS^KEz(-*NgbX}L#@gc Z!GFZ<5ca1yA2I*{002ovPDHLkV1m_q(C+{M literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_scrubber_control_pressed_holo.png b/flock/src/main/res/drawable-xhdpi/flocktheme_scrubber_control_pressed_holo.png new file mode 100755 index 0000000000000000000000000000000000000000..7104290ec11d6587813004b0eded29fca44ddc62 GIT binary patch literal 2161 zcmV-%2#)uOP)% zTWFj`6vuzvE^CbI5@T9}#F{3x+9D!Si`2Um5mdBV1@S=?MSK$RLGVR=Qc<5o^hpH~ zyes&krHIy6yc92`(u-+b+KTB~;}Tyf%Ms(lX1pGT62>NxFvfsSfRB(cCetGomZWF{^1ue* zW?&L4*h(3!DSo18fEcnM-sHD5JGKZax!;=nC+Mx$Y!n91BQ*1Lk=HK!2U*jsx!i zuL3)1(`dFUfC6Ireqb%WEvX@cb_n>^{5@*MT0t*x8nO@PBRkTIFN_+p6ORLLAz`Ynl{7vu-d0oCiFEEWOKo8Q2PZf!22D zj5opzum?B{TnJoEqzyUqJr6vHmfyUzEPZt2yf3vg|1@wVu+)s#3^DH_BlIV5yje~H za2)sx3E?oZN2dZy3~j?G;M$E0WkZ_kxf&NkNbN6i#%jPWWO(+_rLZ-Is(JV8h8B0W zHz1mm0xS>n37~)sp1bC2$jH43{6uF}z8Q=gTE28>vCH9J<{C^7jRcSdZbjDKWqvpC zJ_<+{3yvY0TusO!+Beg{)-@8qI^ZFESGN~;WGNd~8_Zj44_p$J6z7cAO#w|p#NCvnG@iYfX z0?4DpX3!ZM1HNKmtbMDfB3j3su|eQY60{&m02@%GWq&K92rrfNvrCvnG~1<+0j@y2 zd58<3fbw}ar7fcHb}_4d5)L5REd~&8USTFKfECDpT1)|-)4`Hd2Qh)_x9U za_dmM-iI%M38b<+4dH1R+$qF^6+kcI@dfivPXM-hYf+}f+`0h@z@;dk=wt5E%bU*- za4zsMu!C^n*ANe7qL6q5@w%TFAtPvJEXGME=qUg|`0mwL#c_1Ygu%Ot)428k_!EdET@P>Cx9r!Y@3qDl0bXNBNT zd~a4DCV&ing%wm~4M8t{OwlfSm`hu$i1X5e;oSXUZ-LJ^5nrQcjiMc_;@A1IghydVQEVR)O544^ zP?tPw^bBU>0xN(D%9<>)gu9+9DpiNL)3SL+XkyflFMvZB!|2xh_Bj{Cq$ZKRWq&-#6XeHgpqE`j|-vz0-)bEq4up!Swl_wOrc=6AGsUlMu|FP7Iy#H+Ui zKn+Pn=lgPCS>Vg<3*QAkKq}qHCe+Y+|G?anMnBLp{31^g@e25Aam4&^p84L7^7F%l z*90bk56$zNEr^5$Vi5H+xc2NI;uY`(Fb#Z)>SneR;{xD2(y0nnfbY@zYg7_Tv-IAJ zcDAjiPorfu8}r1otdG&|%CPx944egglOjt zM?-_6g+LD~s<`pYR{RZh!Cr#Z&FUdp=-wZB*` z=tUa}qp6YEIgBD&Z4g&O4VBxHF0LnlDb#^kbjJFTi%cU#?i20;#^|&l1-Ho2cIpypR|`w5@fw#NX9Lr%4(Q*+!^IL%Dn5QVyj8#8^+E4RnqXHsL$e9=Y zK=->xvM5U3h}7Pl%+C?cM-uMy&`hW3aTK=hLLHc=qmIlaDAQVvEbXtTgR>hbpl**% zz?p^?(9G`u9zfxHDy_00Asj{f>Sv%F!M4nvh+NMQvb2-H-%UNWb>xu1Z$v8Z24`07 zAErs>q5XFG`+&RXZoB-Mi0K7umv_;Jx=0^l?)H~0u@{LBO#DK72PT%GeZ2F{-+aQm zipbTyK^G28xX7a7#_g!5&@DT;1WphgnJM8PnfcvZcQVnDnSS#u`>A(iri3EAS6F}} zGcNLI3;a&xB|3d*Y68_QUP9q*P}!^#$L4M!Iz5*5vAL~8$L3NwKbWQfEDFdjT!p$r znmIU`1bcAuAko3eB8&LoWD?w?v#U`YF+@~yw@Hh&3-;*jkI0+Y0%mBdPF-|b04|z6 nJY7LW(*MNaX%|%%VZZS|Fd-a&A%#zL00000NkvXXu0mjfqD1q2 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_scrubber_primary_holo.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_scrubber_primary_holo.9.png new file mode 100755 index 0000000000000000000000000000000000000000..8c28164718e8e364b13ec349d14eb40c3dbe7648 GIT binary patch literal 149 zcmeAS@N?(olHy`uVBq!ia0vp^Vn8g#!3HEZm#uLCQZb$`jv*eMZ>JgZH8^mvus>eB z)}-v86!&|RZ@XDJFJJK8c$49v^CDH5poSQpAcj|+u_edkwH@N#cxC1|^UU!w3^}vo ysQzZr_8Z#yHJsPZE)aVVNcc# zU#uFj`0Rtne!c(ivU7fZXs#m7#4_2%+hPgBPRS((ru#xo2l=0??G7LGh yhPR7SBTp=QB`MZJkFnGH9xvXj?<@z#8SLd-jpKeSmsU`k=VfAC?zm4(j<5XAo`s7uJY)G!Gp_R!3y=Gm Rkp(oI!PC{xWt~$(697hmG>QNK literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_spinner_default_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_spinner_default_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..2f9c569467e8618346877f2cf9703dc256e560dc GIT binary patch literal 522 zcmV+l0`>igP)S6h==(L`1qSeFB%hfh$2lL`1}e_y#US@CVmoM5Gj7&Bt(Aq)5rQ$Oz+5%}p|y zN#UMVo6g*ve%#JXAsI4^_dXNOQ(O#~mDCbVQSsQQcx+TW1^(u;6fAAxDy}H~L{Ep& zQSsQQcx+TWHYy$)6_1UI$413NVR8m0ulDMdxXBp;2f$|4*xra+=|fdd^eONL44jV&X^rTuVx>=j z*EaW$nz)rdqfEM0Xwk&2^c67vXIQ^}oL2e*xTzTWC9T=HxYN_mg5mv0-0A5_=-Qo$ zdz(w}`*b4itkYxatsdfyDEK<=B;9JAt|DmSR{ET>r#s!;f))3Ax@)o^iAR%_I&Yag(#>UD^h&?duQkv(E)3rhk<7=R*0VD*sSO6Te+>?OiyxE(L;9 zqEvjpol8BkyZ_t8Ea@{g`oLroR6I5+9-9oR*Tn@-xp(^DyTB*=1|hm)(6B6)i2wiq M07*qoM6N<$g6p^TJpcdz literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_spinner_disabled_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_spinner_disabled_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..fe541269ede8c2cf2eb61892106d3be5ce8056c9 GIT binary patch literal 420 zcmeAS@N?(olHy`uVBq!ia0vp^dO+;N!3HEX`k#maDVAa<&kznEsNqQI0P;BtJR*x3 z7`Qt@n9=;?>9q_DjBcJTjv*P1Z)Z7r9dQs~$aZn%?`l#nuQ&YvzbpU4E5}>jQqBR} zJ$F?Ly=<~&Nt?dt#atDG4i2`IkCI&%E9*H zB3Z#Q>uGhh#=50vFBxq3aqH5l6OUGVX}>(WeOj2C$o-HgPwjW1Q73iY1x0b{zl)7J zsqro!f{y1kWMRu2%4k|D!v9giwF|12|>gUVQ1^d60x&QBa+4skitTaGu&Ft zPIj`H-2U%&c3xngW^Z5b;kW;t9cHIMM%7v`gy#)L0Z@Hm1K0+h0{4JPOeqI*;3x1M zIN=ljHtHSQz&k+2GV5Rh>;SjShO%BhCtYb9g?7axzSTNl4lF_%EurLc>25(IKDBnt zfgivuj8`X9V2?(A3XQnU0xkVFjF*TjcfWKUl>=Few_h&FQ$Ue*;#rclc=Qg_I%J)A zj#RmdXPfva9-L7;G^2QEM)A;$;-MMELoMC;1$cI0nVit3?Xi6`dDzRSbXSG@mAAqhD_Xk zxp-$Nannw(+T?Zi3JpV)ikEh}3*X`j*hfEzgFg5^#RskF7T&&dhLi^U+)mTWJ0#%D zqQ0eKlD<#ii8_76P13u-BR@KF))n6b9s{p|+t&7Zea(`l!?d{Oe`Z{5w3}0>V6B)U6lob&X!dCb3d1rSz0a?R8nzK-GwBDah0`j zC0?>ZFD(F59040~yJ?!BTai>JOwOPYKLs9GJ5!q!Y;y|DE%*kU#d8sJhIYM3{G?vK z1nv@lOtH;5@C*1#?W*UGcg#I>OxhZB9+$wU_zx-@OCoZt15LZ9EX3CW@lH~Zf&{^QE#3E@0NmE5iKIo)q^LG9xRA>^aZPkCqGU;gB#`H}$;{;8x0A_CG7L1t%x^}lml-30PN%~Gv;e38l-!Kw z;{g}~IIt|s6@XBHk#rLPNAaWq--!cg2Ch(p6RqGx(PjrN|YapwaP{Nqx^gQOp6+)2c z2@I+2Nx3;q!`5%E|9X6)Gynm()HX905Bu~gAG{?LFUHN8W}luVv&1JJgJ>Oe*e9%=)QLA@!LVyR8{SF^xDn)b2DV|>^sj~c$Z3g8Ba;m zI2$l)p^0;5+a(^Lz-q(96{gO;`pzAzZ@zl9Jpok~q&)YB*sfnwe`d@Up1N*bO4^i9 khF2qP`>QxlovC12S0z<6wQ1I5pko<4UHx3vIVCg!09Ni~5dZ)H literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_switch_bg_focused_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_switch_bg_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..16d28fd3f493febb0e174fa1689e8b3fe45a1c52 GIT binary patch literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^T0m^Y!3HFciJm?Hq&9oHIEG|2zP)vj_mF{r%f*eZ zm0vdME@6o(kpGgdrx2N-!gt?7`|NI2_Sf0P(e97FvZk-{2-?4Lt;L)mKG(alA6K9L z5x{qJ-GrGRy`Rr{;hp{V*ZT>9E_d$7eKip-)i}G*r2UeNS1}r6m6-0c_Us~AksV7K3YyvuC&le(G@Z0r z{-F1tgGc61DY$%d<5n|!*-fF@v3$#Q)=!B^&^Q|~YoUpAX4@s6rIKF8Q_{c!TZGJe zqRKTcN-FM4Td`ig2~&lM^V0n<&syfc58S=(OpmDZe7?x^iMLh*oy6ej>gTe~DWM4f DcjiEaktG3V_aLvN=*hNB;g zeU+3{oT9qa+B33VbF;WQ$1l3LaN$Dpt!c^gm7}7iW<^IeO`Eo%i9>NBN6Yiwylw&A zQZFi>m)13RAGZAwKA%r2fT!@l-$W(tGiT2HTj?mC^tfB~8S`SVX;!1hhf zdncTiXD!wKJpG<#!{&DLobN_w1j~7*xbC^Xy)p)R@)C4fefCmelF{r5}E*8YtVZD literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_switch_thumb_disabled_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_switch_thumb_disabled_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..c3d80f0eb33fccb012fcf57cd12391fd74731a20 GIT binary patch literal 633 zcmeAS@N?(olHy`uVBq!ia0vp^SwL*X!3HEF)}KiLQY^(zo*^7SP{WbZ0pxQQctjR6 zFmQK*Fr)d&(`y+Rn4~>j978JRyuEX<>sElsv5)W7E=G8K2h)FbB? z*U}PMiJq3awS#BYXB)+rzv~|E{Xb#ZOWTI^QOy}`UbnXG{dlimeX=L-gY+f- z9$D}I#@^m$&LG5~#Xk9!>B9HFOC;D_D%Ad}Dmim7-mAV^yY$ynr9;mn^hK`j*ZF1M zX4JH>CqYA5;uHs!nNGiD<2V%+S_IxQ3OjlHJG0^!-|APNSbZ;VaSdz{IJ8Kk%j)!| z6>+=X{kF55f4=+lQz5=~=hc?oX&RcRi{8&!owoVr?#nMrrmGarjW}%ZplWa1vdmSR zbiN0~n0u8z6=PWZv*usg)~L0gZ1e^0n-;&=^)4nfbnE^1^$Z`piaW|Q9(-IQ$Luln zc|{ik2ZJ2Lms7vn+1v5&1EZSn-BgF@J+IF~m2T77+S;Cu95nV0AHPGXA}6HH1o@{9X< z_X}&+p0Y}yrMo^Xd1c?ZMZKZk_<~%5+3dOgYLodhXYEUU7EwKKe`iPj?Qh?+wnp8# zbH`-XW&_*&TCMw%<>lqiFTeb9y1?R(j#7TyKJj0hM2(Gwjd$-_xBu+(&+E5u-@f&d z&Z1q5Zf&dGe=}!Y=ZZUd+xzeRR<$2v4vFs@#iijvDd$<3jVz^%iTC+ON7p` z#TPY}WbXQUkNs$*->vuG+m~fNYHx4v=}=M%ni>2x^^oQCH)Xp|RNwCoE$R7gcct{~ zIm@ee-AzL{8l5<`MLhz^YaDp;U$4pc{r}q5$zjVl_21WHKTV@Q10$co M)78&qol`;+0J3Z^B>(^b literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_switch_thumb_pressed_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_switch_thumb_pressed_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..ca763f9a30afba0b29e4fa06a9616cabd90229a7 GIT binary patch literal 584 zcmeAS@N?(olHy`uVBq!ia0vp^SwL*X!3HEF)}KjWU|{0+ba4!+nDh3|{=Qod0>?jo zT=!q0KuKyv(40k6LMof~aG9QWKE3$~=cS8tl>}o2KX^t=Ke}o9&ATGyFN=1q&a*lB z{&JAPr|@pyhbLd$pAh%Mrh2j_(DZ-7`gc1V+b5GY{s;)YTKzKe zUz$W)`?Ab;X0v_uE#2Mv?Q50(8((|WrXJ!m#w+GZDjb&6%^KvqLeLRkXG(Z3V;J7Ce;dkUA zM~ssYKT_$12TlNh_>w0A9zdk7(I%sn0<{=#60d`4OpD32hUpJYhNv-Q8>Ytv6aZRK zsl!S23^z47M@fX1yWH7gdk6vW`#4GryB{&`oL3ntooYms z-8azAW_Z``ycTe__sELevk&!IHgSZRWfV^LihQdfIrydjbKfT_Sbx=8iOV+y$x=VH z;OV|nf5QfHldRQ?{HXDQa&)JiQ1IbJ7AvV;^swPNGFP@LV6P|ulyJItbJp6b`kO~N z@q$L(cbmxRGiw2TiNHBYn|szfUE9eDum4s8fB+|)p_4SVC|*7%Hjrx%4jO;su-p&7 zfmxZw4T)BWhGs0mOgYI8rz>~LJv;q6a7jxO%^MfW4py8@CLCHXN6Ia`S6Lf#O(19r z%(NdRmKpp8dvl=TT~P3xtgUOo{g^hZ%I#z#v`%-;Lp;CuT3;q=d8ahAV+i!G zQy~|W;$4!;g>Si~AIc7y^y>3(b(SAEU$C9^TjX^n9f7f{d0j~}^wPvYy zFKFN#RpCygAR}fpDC7otv>K!XdFaCOmvw}(^r#jgMQy((Ek%rw%}vas+~Dv*Nsh>u z%<$mYjONgAkDcF9%yoF|Uq;AWV0XQ2Sy@aOanUTXx21kdIek@?0Ja5`C zne}zYGR?AXKT`G%{dl$k48R!Q>KTmZ*F5okUl6ZV92>!Xi6{Or19o8N5IA(FUm%1b zOc)t!Y(5W;S92RUqm3Ku&6$5eta#RCz5Dra=4i|WNM4z#@yDYcTYio2?dq-K$==eM zJ)-6TeJwkT@!!CZ?`3Aex0q`BbMK(x_P(`Iy>#yUJNr5DHvJMu^qGOXp&OibQGchE zv_+ce+i%ZGWZf-}CLE30zItl|Yg*g-O#?LjK4!^{9V2HN5}ycdGP6(bx^b8QJa?{` zZn7Y-m~ze|iaTE!$3I!@LDn8m&bpFBwo~ zRYqF;q5j6B+w{%C6aU%4Ewz)qWhq20FiVy%4Om@6nUEN^h;lpI{XY=pNCTQBS1rSk zk+42QLZqbrq*;7Igl&@BZm0-f$At41x;h*|WxqHxAFMWeWE=Cy{bmU^WxCqoORJ6a zzvQ!#DPKIvg(8DcZU3IL-L>9hq`4_x>~9}fw{0Ia=jV)vJ@vC`QTgs|bsaDt;K$4~I>cT4F4JY}jWn%8G0V$3gBZI6mR37-0$t{^-!*QVrzCP468oFL>%i^zf4 ztVN0h=W19m^KOE2dHXUk(gcU_GmlrOjwxQp4YRmHNce^Xc=Tp+b#$8yk12I+B`7ljyi z*w=z+r0t$E+`XNBbt`dD18>BY<7UMTlNn?~JpiAV=>7QY4w@muUwp;LMicsojp@Xk zinmlECTp%|JY2a$F*?L^)Wm+V$pK-+l}Km**@=W^d*Pq-Q+< literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_text_select_handle_middle.png b/flock/src/main/res/drawable-xhdpi/flocktheme_text_select_handle_middle.png new file mode 100755 index 0000000000000000000000000000000000000000..59089cd621aeda6179b627b34b34eb6f48b458ba GIT binary patch literal 2005 zcmcJQ`#aN(8^=F{2t{O4n6UIj#=aD>$ze>)p?V_mNJ$YRvN?<>l!=FDq8v+#EvM7IqGc%_#w#PAtJ^BN_KYp+4ec$h2?(4o@*ZY_EQ)jfT>;aVn0079M?5th) z+-q;bBqjE?3?~5&0Q+-M))y}0au;)#oQ|!leq&;H$A{roc9PlI7mLIW7d}?k2*}WW ztlj;2CTf60$ROFPxPTKs2vNl-E~Tug=HAQyEDoy$nT@=Gmyl3W=cbbap*0$EDOEH- z_UNOplvgiCi05Nzy))YG?d?CuXSVusXzuOnB9if7$@oe|(2{73%xfLWz#v-9RV8QY zUSu`UJmJ|-bxoUWgJmI8ckz?UG^c}}cZJ3op5101-9-mEgR=2DlZuZgj`qTbBrKdRDnlr4^3z6Lg_K8adw1Xa5ykw<9NZgLYc0w6h<)Dx^sFHYKLE!)Un$(Y6GT4T$?G zn693m%!nsRuvXQ!Qp13{PDJ0>tz6+9Xca$r`p4wJwXWVyECn|Z!Y3i3V_B-l5pb!r zcdH3~m5uT&#fH z4|Wr%YX6F1j-a{wj*g0TpM!{Mm7;=HWXzS_I=63>2f*{}&;7QEK1@UZp~6p!CoEZB zSrV4KS+dPgDb&GVs`xe#gJ7x-JCvV8HfpvK&SXVT3;sLMb87`(;x8A3J$>*uhV?@& zRZFX{J(q>{yy{xsyE!6k>ih+>^P!v9xCPmJzL9LftI1;*f5sr(BFxX?1hchQ`F)3>{Z_d`gNC|+-1PLON{XD;7 zYPwUqcS!c18|*R;CHJZCJ&RE@sauZred-~oeS$TBU_EQcjbT;6Ad;z-iFICACpq)&XHHd9pYZ+%B~?lHeOe88*@vYc4kiv zUalt?)JAO}NB?@7P+=lP<^q#rC!2thyx3MnOsf zMng_HX%zn<@mq{RO2jK2yh20oBI1nf-xc~c{?E5x{sLT2sprHR!m)?qnumBE$dllQ z2I|p`p$a?v$s$hZt7kY}jBI~@6%{8Aq)IFv&N!vF18Rn;!Kuk#rgt{_Igcc6N%F^tPrWRY1D&UcgS%1qc-H; z?XYRJkrCdHgcZX?k=dW78J$UtCf?8Pu_j^W&?%)b@}ZlAXKT`f+f=M@O9Y*p{ZPw< zWBv2K;sNdxB*G`5zb0tg8$U^R=2KA%+%mj0J zbwx!NK738jOAI{y;l!i25tR*re1Q?N-^`?}!+DcJ^c9pT>hs348#AA$Q$;BESc2mn zoI*^oZ+3oi9qrl)W@>A-Ww=LTIiW`nagwg&36dUOq--2Mo{N*PFcz2`O%x%15*E#P zD@&wAqb0yaC&n`z-FIx9 zEa67yh{B1S0?}C@Sou5RMHXPTAG)*~HjDWAB{w4O&L`NjSbbVCs*-l)E$OEN?FO3f$R#rA6pj^HW}9?Sta~Xq zb}LQkzQddXY7_PZT|gdvTYQpk81BeYB=g$vX?YH%^Nk;ts)~R3Y?t=?3K8?`p^#r4 c_`j9JM~tHiaamj?#9mzkC>ykOxutjfzw`#)ApigX literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_text_select_handle_right.png b/flock/src/main/res/drawable-xhdpi/flocktheme_text_select_handle_right.png new file mode 100755 index 0000000000000000000000000000000000000000..7166a79f624556cf9e2571f2c4ca082a5765515d GIT binary patch literal 1894 zcmbuA={uW=0>$4VLC3CDOGzqK3?V40Lt;s*#J*MRN<~M-%(aE7)IMm((ssm7srFJ* z8blCkEwzn{s(p<;iBQyj{RQ{SeV*Sr=leMyPO^=aso+KFivR!!n%zU&ou|jSLioVv zF2G1Y0st@E3~l5<$YbUQKDKw1{Cq~I)0wOWQYv0W@T&C1{GR)4rNYB=gTpw8pfYYw z>08UWH~alZac6d1+MlpBRXzNJ!XD|l20SVVybU6kAm(>_ZAMoU%XsT#H-1E6El;MP zKmsn;DuzVhkACwghlhlTEp~uiC7yP7&3x^uWjcCjdosS&!RCe{!mN}HveMP`_X*|* zZ{uE53~D;$vyW`;ueL?$_D8AFrm5i<(l6A<*$1h*>M{sg+uO$gV76^+OAaV_@_rIe zAU|N-!O(!@EVfwSy{&_C4D8pjp}**r6cWl|zIXpfuk!(>%cbd6sM1>N8?Hq$YzG2J zQ;hf8svd;sj||nNr7K+}^q%EKw%9vXUr zgICq19$7XlCE>CnJY!}(TXr&a8zOZMS4`hp3?4)lt~`WGHJp0hB?~xKzR%m7ATE@4 zv{T7$;=lLGL8z-d?b?OXBpK$}F-VI|B2i_XL$}Ik@XGW4P>N#_Kf<~Zr?UqSX_7IY z%7t0XwT6vx5)S;KT2@~0ZlbPA^uyHr`{q(s60yXUl-ADO2v=M@1t-&j;AoH(|J>9xc-p}iI(QMw)6k3T9H*%~XU z1RNDJCM_+QxdG2iyj|5kj;j@^bXgb>FI|RDj@6tcbOsI%sAbu>@a0zSEEN#Cw&W)V zutA{^B4YxiEx2P`WlJw?T)@)VyH59=%-u~VtjRe;zJ)zbtQ1~nx_awkcKr5sYMcyl z)9qUw%?W;yZ}5l)N6KLLU?R?(nc@>yi5h&01j+i63p)+M@msf@-yfWvu5WC_xiXu+ zNKs;6Ov0Dm{zB}~a#Q0+db&?*_mB&3TkBdk2AfGnGhV_^7sgRqoBvvEz_S0)7mYX0 z|5FaN*B2;kU|T9daz3=uA(OB!y3wz%?Nli$C@DFQQnpUoa)O`f$WAwE`8l?`bqcS6 z5x4lRi|Oam?4BT=>#gzMuQoTO7Tb~+YHRT3R4M_QjnY7(15QIo{@J*f7u; z3Jgxyf9eslW+FrU`~8#Vh2h)j7F-_cgMQ>Zw6S^TgNuFGN}+dY1b1D)lG{$Xpx;d| zCox2x6{qYG2iiWcmUEWE;X-5+GhcWgR{7$!PH)&;F*SDa(L??b4EhY_2cloUgNqJ8 zIRZQR{h2+A94XaMqXp3aPwUa5F!QY03_u9TCMq0>s(sq$Mv0$DMSd#85^klH`no~?L`Uaq&!D(ygHjUo zV16x`SE89^P@0eAa0@Kn2XMeK2%6Xj2B~d{eQ3Mn$pz(u@)SufL(oRcjb;_(@~rNg z(U&Y*SAb(7`$&idhkH(ySP>q-ibd7V!$1^R1$=M!anggd* z-m37C?i*0huxrZPbskB=RSOD0RIM<3s0+t?hSeF!$QsHRQ*(hg-3+ekf%zFR10w=b zt(y^h9x&0WZ+6|?IOWenFr=$~V@$Q^vJBq-`$}B(lLGW|xo^p|7{|wb6yf}H21Q6= zqk2`lcu4oDAS{TSdRh4SRCnhoUnZ;K=t{`*Ce2-2-f%(+&i7g7UAh|AiF_Jde4xts p*dLu1mRQ@Haeheu3vcbG+$wjf4@W}h6VLw>FuP-gCL4Rj{ulV!l8*oY literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_textfield_activated_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_textfield_activated_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..1c52459ca1e361a57e44c33dfa03dfb335216e83 GIT binary patch literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^W>$AQ@ZALG z=Nvmr(`P<7=Ft;n|Me})K~1)r2R_#g(+cmf$C{rGIs5jF?YiRAT{`A%^w^lV^3UxiVd-tV6pH4(e;#9hF4kdZs?J8^G?3o!y6W}! zrmoW~JEnA4?ThqfURDpF>28-pUw{AmuU7f?I_byzB;Rve8j8P7*b?jpbT)&htDnm{ Hr-UW|9*bQ= literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_textfield_default_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_textfield_default_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..f0ad55a66e15aa967b6ffb7343ac736facef91ca GIT binary patch literal 220 zcmeAS@N?(olHy`uVBq!ia0vp^WGI+ZBxvXlxZ3Yd6R^a%=!5$kknnQSk zq02+94^Uf8AJ9JFL`w#FtTsCjyJsV|Zs9 zOmp{UL$M|tX{f$^f4UkE2 zP{DY--2nrmnBZ^}`>Z;cW=p2!c(ELpXC+z<1IEUiZO9_3ZiEN4o3> zJ-b13b#^}adU5L58ta<5v7=)dU%D`T`PQ9h)vNa|Pu#h2>%{fn?tXJsnZ5L#fA5O8 z=fH+Xf7P!&?dYCaoLib({Nl;r#dF?5*Q5Khjqb+jn={d5b?M}p3rCbsmiw-}G{r{! z@m$ZJ>z8I9P900Hdw183zhC7W)7wvVUwpCSN9Ok}2j|73cX!19ilW}H%@htDe+@ob Bj_d#c literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/flocktheme_textfield_disabled_holo_light.9.png b/flock/src/main/res/drawable-xhdpi/flocktheme_textfield_disabled_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..4ffdd869e10d4a2d36b9248894b2f39b3851cfb2 GIT binary patch literal 1116 zcmbVLOK8+U7>)|16ezXKvYR>G{a^xQ;U%Ih@>>IvqiXPTty%%?*0xp^Xq*6BOgLK9<`(X~6cKO&-*g;V% z3QoQk_h$Dg7IIlL!muURCv1x9*j4gPYY<|(5B57=lKDI_$ z0J}#z^VZ0qm9UvzJL!&+LKL_Vn{>$?@JV3JvIsyEw3t0;hU zJIlu`KF-r|zyh&NlBDglzyqG+1r7)?ASt4t@H|~x46zp21tq5$HCseVG6NX<3da?T zMYbriDCp-vB9Vx62tte?Vqw|CW+~={t#u6=46VTNv4cE1(rEUfVVq=$rwbvtem1+P z*b8fkA}Qlarq2PE=Ug|6tBMYB4*qsyNpzSm`;g1Q5DfCL}an2BN6fbu7h|44L3{ zNmf&70CXVBvXRyV-iT|0DjJ~9&3GX;Jqyc^`8~-O)Q3GgV6yy;as&_!w0KzC7 zKt8Q@x6@hEa=b{6%Ciuy1_S3fw2c6{bTz+*(D#o>V-nV5i##?+Tj_sLj|XS7tlH1py0^QPVW z>e06?_oI)=(Ax z-gZ2k9E2(!5WuW) z@js(%gLT7f>&3BZ_u{A7HpD8OK05c}Qa#Ua5fiHJy@_1TaXeHbzU4xgYRXOT$R!P> zJ7$*nzLlPG|M*$uErHJGB3nN(USfUkG+nZEnZok9DLYlf=G!w`xIOjeIlg0V@(H2q zorP)lyOP_2UrK+poYS@bbFbj-*86IP)*@%SYK=tr8S0*KO**OH5bbx=zbc1&@9e*u zCRMFDTt9JZ^()cl{G;Ko-@P|(pE)Td%<~P?1FQIHCwDA5Z>69qwv0*l7rUvcZp3W9 z+HGMow%ur{jy6dOJHz;QMon<$8@Fwq=Et5ssbzS)!=`Z8qo1+B@MrLJ^>bP0l+XkK D^!d43 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/happy_cloud.png b/flock/src/main/res/drawable-xhdpi/happy_cloud.png new file mode 100644 index 0000000000000000000000000000000000000000..74b36e9368e9b907e29965d940baf4680b2da517 GIT binary patch literal 6134 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 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/item_focused.9.png b/flock/src/main/res/drawable-xhdpi/item_focused.9.png new file mode 100644 index 0000000000000000000000000000000000000000..54864d278572203865fcbc40e2377b70fe54810a GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1|;P@bT0xaTTd6qkcwN$2?zN8ElzxMAm#St znM)FOBs#l4uID+a>;JcBo9$QDM1k2Ey#~GoXHBkqtO<5^vPts3(qh2Ou;@pr$R)nV R*MJ5wc)I$ztaD0e0ssdcC{O?Z literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/item_pressed.9.png b/flock/src/main/res/drawable-xhdpi/item_pressed.9.png new file mode 100644 index 0000000000000000000000000000000000000000..e4b33935a3aa4f1af3fa9e9e199b5c47d43f4b74 GIT binary patch literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1|;P@bT0xamSQK*5Dp-y;YjHK@;M7UB8wRq zxI00Z(fs7;wLn2vPZ!4!jfu$y_kK-b literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/sad_cloud.png b/flock/src/main/res/drawable-xhdpi/sad_cloud.png new file mode 100644 index 0000000000000000000000000000000000000000..a1c1392c00ed5ce11829e99ba23e20b6183dd84a GIT binary patch literal 5308 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 F_dm&@h)Vzf literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xhdpi/sync_in_progress.png b/flock/src/main/res/drawable-xhdpi/sync_in_progress.png new file mode 100644 index 0000000000000000000000000000000000000000..63a28ca68f8354434c91043b4c3a5780bd8b76f4 GIT binary patch literal 976 zcmeAS@N?(olHy`uVBq!ia0vp^n}K)&2OE%d)``jnQjEnx?oJHr&dIz4a@dl*-CY>| zgW!U_%O?XxI14-?iy0WWg+Z8+Vb&Z8pdfpRr>`sfJ$4pG4Y9(PZ%Y^$m;*dr978H@ zy}f(d?}!19+e4K{3!e*CyiROA!W5wIIr+x@n@S6>+5OEvzcd)+95fK%r5Vb3?cv|Z zd)uXdow^-tl&_!I&bgiZsx#CxYo}&?c@mg>>;Kd#h2Zd z6}nAyzZPs%uJ@5B z;>XWFlzO$w^)Gnc%XZycG|%?l$KP*nEINBHeCaxuI=3eikBb#f+@n;Y`thEU@UF{D z6=g3sZb(SkyZYF>$u*JkC#y{s-WKgtonn2s>-k*~W4}kPb-~X(_-BQ5T)lF}$y~|$ zyT;#)o3nq$Pg+~`PHzLZk+xLXt{Ku3KkIG@1crv$>E#l}Cz|F~T`@Ah*#4AReY44- zb1!qwIP6K8cIt-hTvg+wb7u~GGmCmMOD&X{R9Ro4Ekds@Sp zoKV(NcB@I1!3#Mz zo7G)jl3a9NN;f8D($5VN#&z{ACsN+&eYvc3J$$8$S(=V{*c`#1r`AWm6q-4GM)~_a zMov%c-(_?e=NBp$-JdmkW~a>ZZ(LjVov12a^KR126O$y9Td(zhHeJuPqwPYL|FgHP zVV~vY5t^{7a}5`Ts9JxTHRISnHH36+L?daCu5Dur`(yIy`e7e)Qnde wdOt`snk=>1MNZ9l9H~;_u literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flock_actionbar_icon.png b/flock/src/main/res/drawable-xxhdpi/flock_actionbar_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4530571f02f31fd3036ef2f636b713f114c02898 GIT binary patch literal 5332 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 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_off_disabled_focused_holo_light.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_off_disabled_focused_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..aefd6d6050f8315c7e27159a66c71d4428da1f33 GIT binary patch literal 453 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>VC?a9aSW-L^Y+e0-@^$at`Db%nWm|AsDhfBa_0!mHn&EY7_=SGu9T zAn(BZSG)G<|CLoOjr|@|WG5{UR>NF3wQlmAW4B_=6nYpESvZs(Bp45I2)H#EFtrF$ zUr73G*pWNymrXgu)*^bpVP~wTq_niavpWrO>f5iJ%Uh%C`(EnFod)yAYgSg5^A@pH zOf4}uH+xm{67lN>-(NnO+Q%MqeNx!Do5CBee>{6XwD;Rfd1hGHF`PMU{+wN_e9t=*iK2=T!N56>!caP3$-cYa-}n8G`~Ta8>C2(v zbHf0DP;N>o(se#~hmObR1ImEFX&mumIl46t>IS%$7hI;dis#sLAJzw;cW@?1EBFi=!jYuOJ zal8c<34#!4@O+pe!hXvpS~G0>qkRPh`?_bk#B^*JC~6gFiKH1CX*UJS&FA-pZNFD2 zS~9k&xh#rstYrmxbEU@1r`}C)3&;?kE7#Xv7)Mq?e-A z6-QsPuuXDGnxTIphG~egL{d#=B|(;Xlt>|zQY1-1afy=>ypqYJ`WgpuGe{BCWGa&t zWC0|L($KQ&`%Fl^8qe>F?rL_09p?zcWY><BM++G*+UOS9Gklb4sSv{II+8GSr5r z)eKsDVim$xeWtv6lG$j5Hg-S1zPI-A=Yu`*K7|he2*6R$2H-RRgJ2SXp`J1J?CZA^ zin8-#^6vYw7lXInCAQw&`Es?r&K*KJP^n{lyIQ?@@uB$i*x20n^XSw+-3H)ao7dU{ XU_CuD^7zr23p7fQQ>T?pX@2E5pa5Cu literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_off_focused_holo_light.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_off_focused_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..e386ec997e0804b5a0a7eba206bcc5481ff95d17 GIT binary patch literal 535 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V7%w);uumf=j|QatY}A(wukwA zO08WJj4Fhg{x@{aUURVfgwz(6wTjsvlrBlMBunpU7l_$qaO}h>AFiVbE?UKI_pd&! zwK7eAchy?CRGATIE(9cp8MSY1wez%^`X%yG`%*fx!)yGQZ=oUA|b`?gdb*N;4G#4Mw7PHNYd^0_Izm#)3jFRt~UlY3q4 z*SBZMw>;0=Fjlmm5&jUrdGq@6rR%a+Zv7s#GO8uY=h3@Q$Jac!y30Ie*Se3-U03}M zU9hD?tF-=^EX#_ZwO_yedLH(zET7=UV9Yc_@IVhk8jFE)gCye_ z4(bb)?$=)Ax?)Sm;m4C_-^*Uw)gkux`*S`fpG97!;^OfsReRSh`wn#Tmwox^_upP! zC$spB@PC=(PnvpK2FgyYTm%)D+1FQJ0Dz?O9kgtz@dH&hJ-`R+@?L wkn@<8c22HAJ-6)NrZZok)JedifFZ&D59`ft?_B@0h0{TTp00i_>zopr02T}6ZU6uP literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_off_holo_light.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_off_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..310ab257ce7f03bd709c9a6219023907643051e6 GIT binary patch literal 329 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U=;9laSW-L^Y)IRAG4!?%f)%q z-Y1{DA;{^juE?bP(`H7bQjk)=x7&N!gzP2z3&RF%o*$CSW^>sUYD(5DjyBci1 zweiIZGQD`^!oqy?*$UBZ_c{+ m_`^}%zw=gm?q`Pis=~%T;ilGAnQw0mK|-FcelF{r5}E+?Jb9A< literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_off_pressed_holo_light.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_off_pressed_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..773206f631600a15fc76847347a0d6f64f189153 GIT binary patch literal 598 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V3PH8aSW-L^Y-rDEGb8swvWdn zH_y1x)^I_%dcxtIvv)Sd#ja1#OFvho)Y`PcLunJs$%9#(iXvXSKP!Fy*Z1epU*rCD zT)Xrt)R$gi=0702f#uNe+c)AZ8TAh^N)(-Yd|O*Q|NN3E`wVpVolQSs`G41(mwM_! zUh$3RCr!Mysmt)*w4iSVd zGG$`e5Vt05&c6+dzU8Ql!x_dS_z9+hApVP+7bP0l+XkK Dq~H3Q literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_on_disabled_focused_holo_light.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_on_disabled_focused_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..10f9e62a771964cc0f671a28cdb39e278f352e98 GIT binary patch literal 1097 zcmV-P1h)H$P) zKaUef7>A!bA(R49MHH!0rAiSYL_)&L<#PNxAVReHBzy!u37-Ij5~KuWP?RcFs#GZv zHO1*<8H$@Rdn^0c@x0@G$NN6gYFDS78T?rWY79Y1!UfrLlc98o==Okb@bI(I7^9Pf zFBIyU94fOQKK6nx&*Hv6)38MUO?W9HavL}fA-vLl0Hp#xHQSj1$JiIJ^6|y+e~8Cu zOVH{8%a{YlH=fJ&ifyP6LU^|9@7?et`y#R%Lik;)?V9s+tFQV&h{#P&ho9IJk)O2M zsoet>0#?1sEE2<)Gfvry!%=ZhuzsbwaR!8+*o)^D;Q~6up=C(;og(sONt-SpA|gKm zdzr;vh7kTQY1aj?2!B7b*vk;Y>(aI-FF-_YFjDw1aRD*>gUn)T;lqRlSO_2LUI&;( zM6Qz`{;wf~zqB9Abrlf?}O{j6rdZ0B*=ofIYo#PGk3$Mbc+1W|hBr*Hdy>7Dl! zg^%0K4ME-TBl~gAo?FjVbcUT}U$?@?`=5$1C16STiGv&oB60<|pV|DXF@M!pt|~X4 znGql&cUc~OVvvLw{?D1spY?{j{Q_e6*9M$=5HF-w)pHr8@I~Z-h&;%wE1S^;h{#tA z3qNryUQqSxxePqaZ2rY2rkZClx_}VEf50D^#qPuz4p0>S-Fem`3NH7^b7!?4j z63m7Vh6R9%1k>SzaRE>y!F>48fB;xaLKERbBLW~xLNnn*Ljqusgr>rW#st8b5}FGi z8WaFaB{Ug6G%5g2NoY2FF!E{Wy43i$(M{8A57-u5z{t3Ok#PYd;{ry;1&oXf7#SBZ zGBq#6PTjh6P$Cp{`bVgHjJD70_LE@E=ZYl-+iYIC-ur zy+C6KN)p}yzj3m3@g+`H(%mNB0dIkSMr%( P00000NkvXXu0mjfrT_{d literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_on_disabled_holo_light.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_on_disabled_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..013c1f6ee21b95beab9b1cd49a2ea3063cb73cb0 GIT binary patch literal 1574 zcmbVMZA=qq9KQk~3^g*w%s?XN;hgf)yQihJ*IA*xYqe?tOJxX~<7ltt#NL(bbu9>^ zAyb3Sh+1Ex!s45uQJ653i=bsI?T$T+WB+((X4 z1RhrrYBk7&Q{|=-nBaICR!Fg2E0&MJ`9KQfc}k@+2bN&~Lopc&WVt}8mdn%_22VbS zXpOho)n?K#X-kweNR1%4)F|rpdZk{ul;a&Jz;QgnA(Q2b8oBOzR-k;jtUEKxK+4I+9v5rWfYGQA*X-IIwDQ-=B|7Yd{p>U2ip8rOCUX8IpDrqp&@ z>RmKyrrn%}w~FypwMBud>N_Ls-hfIVYNU{DuGsRzzG}xJ*JW?2%TOJ^m2gf6e==` zHL`AjVy$%4E+g8V#L8ZZRTDf-2^??XxOLGED5>ED&RxT~V4_q3n&L97p*S}FNTL^#fjm?(OA(Gm=*~7Z*Dm4r1-mvd;Lx`P9UO zLI{Q=ekcx#NrI*jI^MXl<(WjAWI6Ngz}5_9pR@hloLSwHEabWLVO`DIc$GbVUcsk< zVb9@T&K*#6_%_ZCJ>T{P`+M$XKhaEYo_?d|LFM(4<8FH)2))f*K}XIfpU!+52n~_j zPQm_+hE`&E#hM#W=KC|kH>Zb&RdJ)ASMF|%8Ebv8>&@Q#l9sPWhr(v^t+Vz!*S30x zdk0pVMrWqn8Ulrt#t(1CkVBr0%R+Zk;2j0#oxwR(*MD;lJ}g_+6Ixv|ulM!dj8kp+ zrMb;tX1(u;Uy&R-cHff8em9yOzqfxRAAek7%6_)yYG^R#WA8w*A;EmHy`n9cQk9+& zoLTpC!_%%M;bZ4sx$nr%>D#k#G?>~y?a^o7bWZO(bmPf_M;Ed$`BCMXZDac{&;Duq z*!tSgt|H=We`;~X_qQJ}?+gt-meeDA13_O}nq8H?Z&$GU)$FX61C4`_o@@&Q(@#6% zR)_CM8p*cZ+}y%L38B!DxPjy|A2oJ0`GZY;hiA8!9SU?+?$~5+Ss0!bdv8lsQO_Xs zqe#g)XZ+ALKj+BB<8$EDPqhBTowq%wSG^aT6)TCB?~hx7zx=7)`y_l`xS;wH6az^T mpg0&x@G4=@7& literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_on_focused_holo_light.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_on_focused_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..1ebaf76659f84d424476301a0c58bdd2fdab8034 GIT binary patch literal 3789 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 DFx_9d literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_on_holo_light.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_check_on_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..81ca5f559346fa5b05897ff3d027e3a9bdcf385b GIT binary patch literal 3493 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 D%(f>~ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_default_disabled_focused_holo_light.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_default_disabled_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..c6db766c40e7f81b686abf6ecc4e1ba342cd9d82 GIT binary patch literal 533 zcmeAS@N?(olHy`uVBq!ia0vp^0YIF@!3HFcFAm{mU|_uC>EaktG3V`Fe=ny%8TXIx zr%veL5EppJB(>FQ`h!q{6lteijozbfm2u^o6(J+Bgk=co@g@;2@z(^qlC=l@T?9UD7bbiMlShsf5>vE6h zmmkY`G4hw&_;2zln*L;$(Rq#DI9Di$PGqy zdOk2GV7Ljpz~(=7g`58!5QRJaf=-@l(+(yy4`J18bQam?ulAN-+w{3+@pG=0RgBwX z`;%n6W?u+nt`j-ld$T$suc&_gTe~DWM4f1zXyZ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_default_disabled_holo_light.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_default_disabled_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..b4c90d408e5f33a5854e8525e30efe2aa33661fa GIT binary patch literal 1643 zcmbVMeN0nV6o0J=#2ACaFB+f)P#j4EdP|-A6C)xEB1d33Dn@asLZMg!V+e{!5KMw%B2+=(7=a-0 z_z(hbthI^cdWkdP3-z%UWe5IH?|j&zIc&bSbQ zmU3EH+QHF`9S$Ipg-ki85`suyreJdzjjw?1&T*muWfC{(kf35jVzUME3Tit!6ZKCR zuhn*%Jq}7@qMS@QYXS8riVGP7?%wPupa`NN(pef5g)G%FmU0_q=L}kv5Zs8Zw3R?m zC4!;~DWbp-xe`T{8dR!7Q`B-?mLSFDiV()@Y-Oo(nL>-pP&5HU(Nru&rqn4BjWk87 zKov?{9_G2?(WnKIZ&T4;M< zA1KeuY_$|iS5Q_R%h=!`zXbgn|2iqA)!{fUS7TT(Zw0DLP|FZRi!0@T5#hM4_5W}t z0l`QD&GD~h2_1nB42Z8}?$8MGkH7Lt&0CH^lGE zPl9-ZlE%b>wt=VzNnHc>Bl-R74}aETx_x%}&}gM?`Y*S&$f9o&-(m^A8&Q9 zXu-0yO+rV?uD#vc&)=kXP+P0E=bgK8q@l6(=1))W_N|{KjjfIDt+{rCe$+U6t%Y~3 zM3l4rf@ejYpzY2SeMH{VrI+;!#fM_9F0SF-aW722c(jZW9Nx@(g167?s}5TR@xs1< zI%__h=H*~7#MeV%)0RP#fD{SyfW(^@ki0-o1sy_qs)Of_bZ>Y#@t@lOO5_la@AH1r z{5;K_s~mv{UA<#I zy(|3TlVkT1oBNv&>y0$&y>k1J`ugtJZhwd0T{Tbecxu|n>XF=%o}6#?t|Bc3UDuOG zI3oJy>C2_^EvJ#OXQr&S9}=JV7Jq%>IezKkuLX$G^>ooN=9t{K=c9e)gLf;M%=`K> zPbEHJd%yhyGi+mer#0#JTwc3msbHczzrr=Obo%o}G1nCzhad1#S8uOj-jCSWI^&$6 zJsaH1c)xtP|DJ#2+EXzHYezda)y!#*>l8#;0Cxcrg41AF&%NHWrEL`*mXoz}H%vmH X`c~X{OW&|L@cTEUrfCnT3pV}@h8j`^ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_default_focused_holo_light.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_default_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..345782b713b30414c71c62c2907d39a973bed2b2 GIT binary patch literal 842 zcmeAS@N?(olHy`uVBq!ia0vp^0YIF@!3HFcFAm{mU|<&Xba4!+nDh4T{_M$)GRHr@ zzx&xHGivQN*F_Px9UZ&4xOUyJcerr&=2730LIL)PevedG)sL>-5mg}kk$;7^sAJ2k z0F|Wzm*qW$XWN|pd}nU+>`9mIEcg99<$N~Jou?;fd!OikzEk6+@4B;ZSFLt;tUldo zvG+dv#8MuPBnJWJCJBKf4GL^5h6){wjv%24-`hXS$3L6LxBRH|uC0YWH&yn}X1}WI z=f3f*Z0y>W$M4U@&8m50+#Y`5{Q33qlD5SUd%J#pHtRe()8$B{_PJj^*K57HHP(g- zboPCEG$}16>QB|{M}NN;&-D}ib*EmSEqGbv*_*$-tfJ;<{yuY2d)o1Q@e+xrAveBz zxa4(t+&WpY{`vuxygA;|Q_mb17L?RIH|4UM>fNj>>Prq+D9vMheKsXwjmhp;#@obu zr>2Ex`x-wmzLw7_H9v6g*&Ai?P5I|vn_k$me|>nI`^DPwe|r;WAK#+8k00Sb0p_G1 zyA8S^p2gySJVLr}_4Ag=KHx$aHbE>;mFXdp03O|-Fm{6nGf?54Z|8)re`{Iy|JxF) z7kk1M7=Amj_KwNd+qIXqkGLgkD_7UPi`gq!z4b#iSN^o@_1(|?=SiGfex1!ewMzUn z|JT#4L8^R*x5Rvvd@owy`fGXp^z8G~u1kG;{@}<;=br|=TZEQwDxB3@?AKGeNpkta zs9M`CYFFFc*_PEl`@|KoqIUj$u73L|ODwmB^tJa~wwz!2EqXytwt?3wM-dy%E7P7m z&F}B~7WkFx$G6!!cXt2EUJ@~ty;k?6r1q^FIeS&Pw%l;&mgQSwTIX^tzGe#ROM`jB zg(r&j=Iqv9zIL+WW0l3)0Z~z&rJt7SbRFm0*Qfp>`N#UW-3^%G?$KHDZ=%cFzgK;K ge_}stC;N{fn&&#V`=W@mz&yg>>FVdQ&MBb@0Ht7Wy#N3J literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_default_normal_holo_light.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_default_normal_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..0dc089d4794ace57b485cde03884d39507a6e9cb GIT binary patch literal 862 zcmeAS@N?(olHy`uVBq!ia0vp^0YIF@!3HFcFAm{mU|?4Dba4!+nDh4TS#OaZMRf;bH0>$|+DlXc;z)!YWZ?08q$(tvtFXS@o9fD*RZT`aK*?!2m z_=D5d;#N^WWnCpO1I{GCzBrx;|37V%yBSCnn$0 zx^`M(_DT28HJq0V=LOc_-~=i)+x){n3gQRGyl)0i_)!k zF8Tad++D$cz?UJ-mf3=}=T+f$-gGPP;`7fY-m74k091Ua==#)=>EG%&VvpU>jMJZ+sjqJ$wRP#uO{YKlF}fcU zW3TS|9px|b`nkGwpXlwV%_n}zJy`lA*X-IM6|t+k9CvP#sN>p{zpVMF31ss z#yoV2_y66TWgKpD&Ht&{iAa6J+?f^<^ZDNM?eBGCY`M|po59#3UcIO@xP9^RtrD_o zj6SE<9geL3Ir03$B7qqaWgY2^uca=q2<~GEwfPt3>@BkVvr^Cr#hdfpl{*9y%5zrC z*83H*o_7Iy0t5yGpw3zyYKBLUHJ=dEEaktG3V{w^WGwf497mE z$EDx?!?=3uO7C{PnJx-KDOH{8?jCmj@92{LQlF!Lt`+CSgNObxtiPmc*wL}zgn;#o zlYP0zEM8j~zb`Rh;+KB=`^h@x^X%U?ec&^`FI$#2?Pj@+jDO_h?|ZkcYn55cbHZnN zxYB!(2}>GOSXDF?JQ+Q?Jc1l1F--ysJ@5W})!ch?y43vTd$(MieBayYd|K?$7u6ph z9XfG!wy~dgf#jE07vFB=Ui`S9yD(SC^KMFJ&zY!-4SL<#yLs5=8GhKs)o?%e@u35s zreFR3ec3zv`rEHI)l3(C@ja_}*|$O)&u@l%Hcsfz@~yG9=Y5>-`z5)W??5Sor}pEy zOm4r+Hi+c@p89GLYtg3Lra*-WX6NsITD*5hkJw|zKK6<8zg}4%BHlRX`0uk}tGa&# zo}W{uw!ES6mC>sT@7&Gu2mhbbTyXYN)s@_HrL+BFI?96K{$hPHXV2w{$O4#t#}XVM zp_2=K&Y%5MK7$`gN8>V~g(w2h;KO0WaU_SR>}0+2H>Tc#)#uV=gV%QxJs)lJ&HK#m z#+&uWFuB3`_7$P!p6r)5NbT`(V{i9zz4M}a`s)t;!-hF;O>%aH^Tr&rX06$DX-BlC zxO|LWyKwf>iTmzdwySx}80VL_`J|DG+2MC_P3x@_e<*$kH)}rTJl%C?Ug7__*|w9X zYt?Svow;yM%CVQtFHG)l_RJ5Lskz0I{w&`8G=tNYWT7aAhasv_`;A0wS0C3<6)X6| z_fF!z%${A`H!Qi=oVPx5nqhlqIHQVV%hm*O|LoE^U5{$ye5$xMFyFnwuf$R?<6yW) z)vD|D3oujG6Gry*E4qAEuja>EeQHRX&bOZ-SngaB?>^Owz?{I~>FVdQ&MBb@0JR%q Ag8%>k literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_off_disabled_focused_holo_light.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_off_disabled_focused_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..bcb3daf898df36c6544f95a85aa6aa1faf56eeaa GIT binary patch literal 2485 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 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_off_disabled_holo_light.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_off_disabled_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..06f0cc78c708280e2c4d6816f58a03c4073f2915 GIT binary patch literal 2208 zcmbVOX;c$g7Ot=cnpFW8uq^_j*g~oTSsM_tLC`=3u-VihBn8Bng(Nf~qNKqM7+JKD zvD+RHP;mqX6;RwrXIK=_cyM8kjRGy8POBh_GKx4A6?=Yg`p48cRqx&R?)TmOoXY1# zgxXp!wFUsdmJ`N|Lf0hYi?u+%zy39of-Xx`tT&a=t)}sK8_-RU{3>jbCWPfg)ib zE|$oJxN-)PCJNIikZ4Urj6fq6P=&a)L12KIh7w2+6(3Yfwn~*Wbs+8oFAd!rkMTJ0 zgNjNVi2LYN9G3?&WC{c%`a(Vehy;No*cbNmCzHu*Kmr6qc!+?92|h5H=0~7G5cuiA zq1F__6j~IM{mB+81>(|FDme|0&&LV(sI#bXYP=I)OT85L1)XpssLS`_|Prc96_L8K}UGZ2T~ z_zFcrnm>umBD3jK27>^TC@@T6(&D!x>J%-I#8c0XZBe~6_q6bN4>Q^d$*Tjx4}mnKumlxZ?K z$cQF_T)sdgHOj{Fe2kWfC`8{PLbgIC0YBuICYsN_AC(@$WD#MA0DXv?46~`QACc}4 z!3>y<`u-6s{C{$WN1eeNo8y0)W$p;=K;!mv>!ZTw;X$P6#3<0wNNyVG1OT%e9A-$2 z`tFmT9!ExpICUKVp69&PyE9p)PObX6zhzU;fe*0`d?iKLc z0-x|eTpLxa6Pjb3vVbai#@wvHs$nV5h;0f5!w0Fz3r7FadEI2qc!RUp~YcUdz#|KU50ni zV`o{NkE5<4aR$==C2e`xlzCasbopS}_?@Jx%vFi58lI*0zUAH%wJ-Os0!r>|yLop* zBoc0Z-IRV}fBLZNn(sE>uyh~=eFY4z!&eU^1o zCg*IPJQ#JWBXSu>R|K&~*KI5pb=KIIjo-GLT+BL3t|r%Y4)jEUYtM>8>{Fgf`F=4$qLnmf9?X!)ZS#5fndr^E} zYq&Wt?_hO-uA{8?9;So&UL1 zA<&y;KglyVnVXHs@a+pf@_-M5cf@rI&3*=i|Fw zIs=0K+;0c&I@nY!I_|Q4r{UMPS=RtU*qQKzwzG?b&s$^tw7xZ8uq#{&x-0FpbsEbL0?Eo8)#->NZTCPET%r1CY z-=+@W@VM1JU`wEKOTFT97g5Ga`)1#mja`8zm)&|Wr`A#TWuK-47@)NL!%^2V=(iUb z$sX*(Rt~*(qXE>xexivy;<&SEnAoB*w}}C3Usav1HEH}V9X!&s$Y0=apA;lLJ{@Ry zSI`vduKimTq1LQq%87kb6P$g`sVrm#aL8)ZVYkK^^JtfW`ryTpVXy@^R`Tkr$L7sr zFFm^skJV#WQk93g3e>?EEoS&nz>>cng39Y z69*jYW<0Z%p{u?qGYj_5qjO$gPR$+=;6u`Wcr6P==(;{eHasEu}t;O$GUXpiI=bHrsP8bJ(1(*dp0vO)>;^=`LSO~DF!`$xe RX7Y{y3>;Pjvw@zJ`(GyaWLy9M literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_off_focused_holo_light.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_off_focused_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..947b1bfc1cf0adc3833282e54458c58fb88d641c GIT binary patch literal 2694 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 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_off_pressed_holo_light.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_off_pressed_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..9fe2ebfce804bf33cc3663d5d825e96628c83f3a GIT binary patch literal 3039 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 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_on_disabled_focused_holo_light.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_on_disabled_focused_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..bdf66262114fa67114268c9491d031ab3cd870ed GIT binary patch literal 4460 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 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_on_disabled_holo_light.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_on_disabled_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..adb73043968742c325ffbe61821b15087449408e GIT binary patch literal 3894 zcmbVPc{o)2|35=XqwLudVl_kW2ixvu?QQ0I2mlj56p}837ce!Zcd}A91g?xvwLov`F zMtL|oL-33UD#X+fZa{`3;SeOk5MgX$W@e@jF@huDFt`y6VPt?XLmL~R;c&>`7gVS% zf)a>!A=vz_OSr;7gV<~)8V2KVIEEZ!Lq^0g7y^YtZOAY(G7usRSkZJgiEBV-?fxl& zKxL65f|=}K1|6~?krcp)WMiO0OMkdPV>&wiBbd(mJ5a)q!MG$Q3}Fa|(P$ffeMhs{ zF4X^+@vmr>TQrjjbD^>rkr8BJJ_2|DBnxBr-xX~Dh1Q@CMg$9!LJA`=$dNQEooz?J zK!tA%DZv!93DS&cW`jfF@kR)wIRar$z~Kl8Qyd(JG$L4AoBx#YZ(NiO!N}Uk6mNsV z3zYe#3XNpv#xr`}+p-oLrH|H?(V$9 z&Or<|gB8SJLh!Dp5JwU@n7*;zNY4*%3Dk(-C@RG!f1Q*&bUv&>-PE~_bCkM3Qc zy2&$pqFk71Y`4jmjnlm=J(x7_M>`Az9pc2Dwp#X^!NBG?=;k;PY`O>*ba_AMGT>Yc zI0HH^fDRz93&;aW@&Fhh{sTKix_--9RDEAm80PGfG(+RlVzhE~w#W3cpyklmrtlPV z?Az(3$insYlgB_+l5UPePDA!@y>>cBuUJ;1O{wd@QYHbUtJ=Z?mcFx76C+4>j_O z+ZOe`V}9l4n|II8Hb8x@zIfVPxi6=B?>gS19N^38bEU$!HysV2I##`ReG4{u#z|Q+ z@#bFiQAc;591k6Qpm#kTw5BbJXz)DJ5(ogjUqu(A1h?_0nlf$*WT}-?rKZo!UHwHi z_w^GSQcq#yzT|x_58iPi8))`b^^cW$yleMKTYHE7mZjzI(d6aaQ@jJ%)t-0dr$Lq4 zY93Gg=QM#g=f2%n-oE+;&3=@?Ks5+8V|ArSopE-l{qa8t_`vEfIhqsPYnT6WuF zBBM^3JQ&I0SXJxz37ClqkrLh`*{P(ma|?H9gZa#q`>K-Rf21ZM~MHTXH_+CBXrr*uMhn#h~J&I^W8vQM z<-KZ{7&lWSa;k0Q!6=h>C!n;##+OfLFw*kO!vog4a*V?T?{B}rUao6+GdrtC?K}5! z;g(1LA%uG~y~iS8p@|6!U0i0dSXzA*zNIJDzC=30mCAAp3YHD^_1(tqm6Vj|15_$? zF(V_xfk-5l-MF#W#ogVkDYUQRuaC2RqU`)Y>Q!1?``*9a_Ww>*x8#}IU!yyn{^pyq z!&x7=ff@ubCCOVS!_5j--cLSm9334UNKa3n3NrC>_4b~fd;NL=iA3Iw^zCan%20^g zuPJ-5#3g5DX6EJV*Fl=n;sgt|gH@%av(NG|$3<%BufU#Ieua4M?`gi2Bhym>-7|pd zsK{%xyL>a*f`TjDFAghngEckVBqi_Q7il!w_Il2Ig|-AtyHs(J>!T^V)!N>#VuJ0P znVl^Wqh1P{*B)+&9#>aaF91n@U7lC3XO2G5@7C1-Y0#o_^Ktf6(ceNMb-1Qt;3abyK}unmxxk{+Vh zTz--*=Wj6aCd99ihq5DQjT?Vhs}pD+G@SZP5j)TQT6De!4!+BA+XkRlKKtD)a@2K+5MKeHzsRNhDeysKSt9Vbv=`B)G z{GEDN5E|s-#>Qcfty7RSl;DBt%$nZ}Pm$qgt`Zk=M|0EAp~R2V08oSPy_;87o26fy zDqoR+)3B!NgwBPAX9J?8n@cxUbWShLC?&_&a@SX*!Q=dRs~ld_p8n3y>U!#|%CS6m zTZMo*o~pnj_WZY|h{>e+7p3lV#m#}>e1l>_s?p`LNo|4MqEDN2 zu9vjC^pHVuySj@lc6FXUkY4xe zZP~_Tgc9Mz*H3f(Vc?t{6nikn_{1%r)8jz1(=o030{{FZvrE&iH~Mjw^`}IFwRo7J z_CB4FHyO?H*L^24RQSEPtAv$6>3EA}`$mOFpsIv4<5HTLbEk*s?xsL-Kxb%w{ZmX#tYP9BCKG)()!`V%C(ZYUJpgfx#P6Xb zPac<~)pt<%?@xI%VpIz+-2xLVcs|0!eEl(=S4BtNH`dBQ-(ChgAnCOlY4R55j zobrW&2q<6^YM0QZlzOFa*3sG7f&bM`UIJ)`<{VH<(@wWtnKHjKK>!pqm0OaM!$Ly5 zir;Y3!6gX+Aiz_TRajRSjYtc>IIYHl0;Ef-=xL|tr$o0v={5W8Uoig6y#h(5w0)Wz zRNSFAFL$ZqV*PFoKGm*|*>tt*sgWY=Xo~O?_DlO-d@B<#^itE9JM*;UdewYpb9AZz zw6IkD=N=$7a0vUT{GqeX1k4232!-yEV0W2VlhBnsaqO9KY)(6kdo!MIvy5|h@6$Yd z&DagHLto*Km*-54YdxiMpvzMYhgU2mA4p`($v=7y67YsX)b z-A>s)%-q9^CZDuDtk7DuzFNCM)*e$9GtbcgW$5sLz2)H=9lwk6{Kn{|z@Yizd*sYK zho0_k$AL|)R=%f64k@XUTMt}HiX?*bL+dA1RJQ^~g+P7bd5eDMBg#R$Q0&*zxkY>0 zy{ud&zr4@`k^S{k600@v?Z;1dJ-^SAJSXyve?`e#W;gF`W?8%{@JBiG%ZHh|hi9eI zoU_oD!b2{&Y0cD9{RhwSp7m2nR6h9{_1)$XxSr;y`(A6wmRBtd{ZdHxFZ@?Y*bzxD z+bV>ax%oxNY+mK{>p=@EJa4wI-?IwM6C7Pt$tNz2R;O!4YFNTY{m5Z`RGX+% z%tRKME_%ksHif5g%gYlD()YL6S~etsb)-OJ8l}DV2M+u`sMa_UPj+@`IP+**|B z*ew{tx0f|mnabEiT%7*oczaKL%G3}SkAEoGBBiE35XTp7X=yoga=a~dpoTHh`+4mo z!$jp{2DLtTIJXX!z0h9kQ=vpf$#_|(=k4Q&RUW;k5=5gpCT5kN)60x~^IW&DtE=nK zopnY9s_&ecZM5kbojKm{;Go{@j+?ZD^Ru%o%*oZ-5FXEGAiptkw)f@Y_@n9LF)<1T zop+X2`jfAP_^7L>pj+y!%GMLBZe5F&0F;jFwMm)md0iyd&FUDx0A3T_zDh4l;9v?m z!Ie0W^V^+9weVGl~YP&){AUvKNDtH>tb;YTb)&Xs7CWzw3dH|*}d3r z`P&vfBJ-gJyE_70kkcz76{8V1G+gy-dSHt_$O##-oh$oBQJ-q8l!N!mps#mZJxA83 z@R~#wpiiW3J0?Av8j!krLlv}~9Z;XwEfOaqm!D*%b{J^Am0m%r>7`^{+o0*`7%QM2TdprkuJi%EwARm=Pa3 z;&$-B*6@%0Ex_}LIFoqFr*P9^|FYa#il8hvS*Q4J+jvOVngwWhFlT_!*PvUVXwn*X z!e2n@@Wbh}FVtQ_6so=a&?+C;x9L+A(f#*??8Ul|O9xclA2b9M1qcLJ*9F6E88IP6 l|F?7Me~z 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 XGFF(cjcTAx00000NkvXXu0mjfc1SAW literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_on_holo_light.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_on_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..9a46741e4fadab8eeb44f4d3000e74a89035753a GIT binary patch literal 3510 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 Nz(B_siq&$6`5&in<0Ak7 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_on_pressed_holo_light.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_radio_on_pressed_holo_light.png new file mode 100755 index 0000000000000000000000000000000000000000..58108fc4eb67104552a84d1176393fc0cb72da72 GIT binary patch literal 3544 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 Sp&$GJ0000X^g; literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_off_disabled_focused_holo_light.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_off_disabled_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..4b334f596b99a17b2642207281a6fb06322ea57d GIT binary patch literal 615 zcmeAS@N?(olHy`uVBq!ia0vp^0YIF@!3HFcFAm{mU|`boba4!+nDh3oVb<*chW3yC ztE7E2)uN{d)HKK}F3!JX>;B>1>=M0s#Z7*4JufdgO`mXZ$;vB^x-%{X{QjF7t#zN@ z$KiwAr{j~~C|0d|5&rwg$E+uxUr0n{ye^)9QEGq=$?f!8L)+@#$vKt!N|G3K(X%;uNtud9CO)O+UW^}cbvWc{94@dKeJ z4_s=P-uk$0%?sP9Y&v_tv;!GGP1UY^_lo*%bARKR-?L7LN*E=aXi#Ks;~|TYHLp%1 zvbum3p*`yklgcA56fadD;r{*2K9~JM4Z-n1x zd~SQ*ydv(ZBkl9nOCBhc7?%(RS_KXqh{B|%tekC2BZ+^z>7~jRWZIQ8? zH6^2e&5ykNJ%8r@s*_j0x83YtcAM?zM#UvHlE_VcXW`rm8nzBiiQ-FBin zd!a7xk81gsoOjI*eLeTlFr(~W-ph9{`DH5PeVClYS1;?fm{w4q_2QAKNKMBUyN44# xreCZ|K=W8=!4LCSuYUNO&wbiH)!XV1OGmHw$9+pGdVvXu!PC{xWt~$(695+o2c7@` literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_off_disabled_holo_light.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_off_disabled_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..9b3900a775507f2bbd7732fb2d04fbf1be920265 GIT binary patch literal 1662 zcmbVNYfuwc6kd=j>d07aDT>y0aj>TMwp81+Q70DwWeSX3k|`DM)T&GAUSN zHLRnODT683agmCJpb2I=C6!CnCXzXxTNv)%8GGwH_ z34v*o3|T4EVp_X`%AynUSW2I#OEKnU8>J>>=@K~BO#%Wd#Tj6?)nap!ZW-d|C4ud8 zqX_H|;j(4OSW&6kBv`?)6fERnF-A;;!6KZ8^95qDcri?1IEG>biW4!onB)^AhQZ?( z0DzkIh+gum5#MwwQIGnh;7bsp@3wl+h9j=9)?=2zP|j?PA-}Hw~W_D zJ5zG)6q-yqnH<&#_QM<%00ZsboX8glsv%dfG}sh_MadX*tdxz@C}jxv!ZXn(62m1J zj*A7Dn7~959G56?ff|>`i}*sRfG-jUI9`Vps>A{@#+T!`l)!NnAs0&25=_d*o;&lFAegJW2JAzk_whGa15KmFqXgsBxNxiZ;se}oN}EWaC{9_ zb>4gY!S+Rustb;G-Jkc#;f}<&t=qj)$DDaPTklj>b++yKXrOrT^U{m5q_n2G)x3Ra z)lcPTej{VQsi>)_7kKllRoQKW^O3LiJPA280ksxxO;~ujHoSXzSr|aDM%f zVrhwVdK?tA016I;Jt4pfDS?0m%>dT)xL{xfCqTf0CVT3G6AcF@_}v*gPeWZ%*qGl3 z4H_qLZ}l8KUwGi<4wqh%35Ax=&-lZB(>6Mo{=5iwpX%~(6s6$`ncbv54Kny2KCV~w`v-`JpO*1i>gYCExA8P?6Dpyj7;6T=9kUA-M8)#shjq* xe}_->O9cf#D{SGHOI@u-#$%=DvGT{cDyV9D8z}%@KLFJLtf>hUf#)528}*n#2w?r=45gSo~C6c;1Kq zN1Fe|$%V45W_&ODeA${3|Emp~AKjS}yO!r?utiDclh1o4f7Z;Z5r3OaatGrbmTxbZ z-@WSF$NgZ2-95n%l_v_=W4|74QU3L`o~`Eb^JQChe|own#{XE)7O>#E;pB$h(vtBlcx*}Ob978H@y}i5N?{xa*= z|2-FQY~^U3xR<{{>m$R`=um-(ERTzq0$z2&#^wJv=R5{2JY#{Swi_d~z6fI&9{o9n6{-xwJf#d#RTGvpa> z9>}}x?qIgz_BX)^yl=MEvZOHY&W-=F_d|uvwC3jKi_e}tyO&^4p{aB@?oq2j_N-^G zORWADR8&}4+S$#!ZtTHx@87?F*S>xGCcX5^D#K$I&({>6`_8HFDKu5Zb5?5Pv%UZ8 z%F4d|d-38$k>8OC$KFUEc=z>dX>!j_-P4<{MMb}_eIdLc$m^4h|MlzF!#C-8GrnQ^ z!ExZ+=D89F{s8T+ZT-vFf44i6X8d?Bf8{)N*T6jt6SgzXkV+0)wd#`B z*Lt?d?77!&NhY@$${8|oHM9s^*naujKD9Q1I@#%2%m!6e#j+;^CN!|9C^$PX2~B9= zVdV4xF<5{ijGP!!2mW&zOlahV$Zb$|_;+t6l!I!<4-v+nZ!gdHz(9+HFhQY7y z{@ar$Pu||YH#0r``Q0GqO@$L1TA~xUg!^ARFxmYqP2lD{ZI>&*rSV?%(>c{z@u#|r z{VLywX7gT&>h4HUS~OYZr#!bVuoG34(tnbx*kObiU>2T z{_!*Aeq0E4;jQTgTyEml#-h2Ha`&uoHH7>XKrU27FgQu&X%Q~lo FCIFy&bLap7 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_off_pressed_holo_light.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_off_pressed_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..c9edf5c8785a29d4144699b85cab535c6fb1524a GIT binary patch literal 869 zcmeAS@N?(olHy`uVBq!ia0vp^0YIF@!3HFcFAm{mU|`nqba4!+nDh3oz2EHwnPVU4 z|9@c78M2T|OUclndY|8=Tw@`*~}6PGWs%khWR$W-xZEqb^}ed8+$fg=qH zY%GQf9gL1V97zrW%uQgS_xx9nA3vVIZToinZMWan39!5j_ItH-#j2B^YYGYrKR$i% z;KB6s0&$G|{QTR0{r$JxqHnir;Ehc>Ym3kQ?AfXjDB|il>B!nJ?QPf6nh!qseDUJN z)WeA;-)@wxS+zSnJUlf}~1ig?ai_VL&;x8+x}jw)@^R*H8$VkUW9 zJ;6w-_gZLaQqRg>!(Ou;I&+FArOqrp=X=?N_xR(F*@B)=SecHcMwaE}<()E!`CA+i z8oEt`OY^jmXuq5tqjRX}^EoT*zO=r7RdCKT_Il9uQVqHHUVF+EL+b>8i#s>q@;a8# zz#*hDrM-TNPw@jeM8GWw*ulclQjH#*I1D(Eesgkqc~Q})fARhA-@mV~e!0`84X*h@ z&^(t#8d~DE?UlNz2lnpW`@VbL;XQl!9`0J_{5MXSd_lf7kQ994ElyU1?O+vnne2Oo4^WKZ#y9d)Fu1 zO}bxf=h<{e|Jtvtvezdj%{%)%!8JuoY5V5r#oPV4SVVUAPdoE2{A)_k>DnVfHhl3< zS$|3VTYX(P0iG1$VaMe%t6qh3-v8^q+&`03=kwJwMEw(&|E$IY%smVYp00i_>zopr E03`i)iU0rr literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_on_disabled_focused_holo_light.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_on_disabled_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..81adc747fc1b08bcea0c6d72219bccfcc9833786 GIT binary patch literal 692 zcmeAS@N?(olHy`uVBq!ia0vp^0YIF@!3HFcFAm{mU|_2Dba4!+nDh3oVcx9(hGQT1 z$7i+}Es$BT%EF3RZQTkp*I&(5owh+cexy1SopVx3*amp*!u(_nAK= zYwb@mExa*fKhOCEA67_&-z}^*{rvWZ=9PfV$Pv87fZLhV}uz)SW zVlUA`th@G4$+7*xfpB@?f#3!Y!X~Sptrf6ZxBY0}>&cp$3QGzax5U5g_;@0yMTl9? zWAei-%5Dke5#KBx)ZcQyr&>_^Bwy*n-vUVum88sO4`cQ4>Gkt(P1ep#cglYGsNwmt zB@MN1Hx{u8XlN_g^lyvp;*L0_amVE*^L~%Txz{t#M9yblcl7JwiL5nBccX=upO4$lt}E93J|pGw?%nyUU;Yg5-o+O%9h)6pmm?SXbWQZ4v&ySyN1Iu`e)cc@ znovaD-xI&@PrN#R#xloy#~=B1_eHgQDXA5`v5(KD_QxUfOH)qVR=mA(;W^$Ld(w05 zEIHTSey*@hZ~Kz#Uu2o%*{3#K_Z+gjM2P#hEjQ(&H+;+gQu&X%Q~loCIH05GQ|J@ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_on_disabled_holo_light.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_on_disabled_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..922cf4ddc0fbd7b358618741d0f3a8ebc6b6a5e5 GIT binary patch literal 1032 zcmeAS@N?(olHy`uVBq!ia0vp^0YIF@!3HFcFAm{mU|>G)>EaktG3V{wjrq#1BF8`8 zuijjkQ`{$bRkL;dQkU=Q3S3&Vz6h!cX$1Y7C?eRt&c^lnvW1Vnq=Pcb&{(6a$Wi3HY|)S=C+-=>5Nl# z7EEFd+0@`A7I2bDE7C!gdqoOJFz8eFtHp~KpLbh)@%r7o^7%8_U(dOc+-dl)xVZT9 zw?~g2O}~8k@_wPV2}>G|A3yH@{rmU*rRTmnad0R)q}-I2uT)a9c>Z9HGsi-$)1R)M zJb7}rX!}H^rw6O|-kZMp=9+MkqweRzn;zAG|~5fnVjHH>%3`8+QhR>n-b~~X{2BA@fJ4%O zZ#n6PZU*a1mGd6E#PR&u&-_yDUQ|D0{(n8qNlaDmwUu7^F2DRQ!64$aCCh9B9VI!2 z89xK#PJsk#?ApKAO)L7pryLO5!c2!*OvAxDt)-OcwKgj zz^cugH&18y7gsvtNdrScLBWT!@7}!&R{Et#Sf>hij}f5r{7pKZD%u#$22 z*`=3bZf!N0eRxszmeUQ#AD!)#;$&3)YI5Q%^ETm*3i-U-Ol~@hwkn1%i`;SRaL+f^ ze`del+q$W|nRCSM;ezDmdKI;Vst06&@F?f?J) literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_on_focused_holo_light.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_on_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..a10d01dd43b504eca8feca3e1afa9786b93ba7f2 GIT binary patch literal 769 zcmeAS@N?(olHy`uVBq!ia0vp^0YIF@!3HFcFAm{mU|>4o>EaktG3V{wja|Zl5^WF5 z+j2OBwnT;rew12s#QTGtj?0#f+!5TFF45lk$4}ilLCM|wKC8>|`_yO5VCMwE7D^v~G>#m*FD?Fg>6|`aZfz|u(>}RoKSp92K z>l{8MRf8q9OmjBv%dg0d-qL)BNp9k|LuQMsX69_Op3Q3+V#yjaxo_L_{}InN#i?~O zb375=aI;TzLq=qpFV~E-j4Z$GxmFhk@p>5U&x>gn$>p`VyJKE!#(_zHZ93BCdarwz z89BGnWD#Tdo98d@yJSU|moY9DOgnTVHfR3#ch&E;8@pc|IM>Db-eY;p;uls%7w7!# zd42p!c37DKXS4GMOh%@-TQ&!zyFtA?)|f&XvyJx>Dc1>uL(!IFcL$n43UC3m))aU3@X4qM+czvfFR>^(oMH6E<5^qsqCg}+e*S7sdy$mzjOZd>C@-c)zv4ro&K@o zrEGenSoevGXV3PU9n<+_6Q8kmZSlI7LD5U6OfvC(T(^J!Y!y!(CHKf_y~iGJ%-!Vo zxIt;sy?giUKHn3M%3i)TucY0-wb*ak>7}LTbT65Ne*RgbptQ0h$0i}C=caC#llIkr z-xadgUXAKDT&B)w>%^rYDENNwmbaPJ;?L()ymt-jUtQRLK``t8i?y824LE#`B`|Oa zd7S)f@3oBm0Sh?Dz^Xhr`xzPwn2>`Kmu_HyD?UG#b7%Lb$IGqPm8DxRmzS$Gu0P0$ zh>Q!9YsBqtTh{J>Yikp7fyL^1NbL3hXTDr3T|1}Pv0pudY3ns1*FY)PiSJ6R*4i^a_?_P96|MaHxwYhIqqOwCHr|n&&@$IwCdDkT$4O)B_%=J+d zKm71RX=rG*=cFlxGPgClqD(vQRj01)?KQN%7G*xyPk+h7kMorh8NcS3?KV1ABg@0K ze%IZ1-3LnNF-3=7?SK9Hb^hm{f4p3z?E9Pd&77JVsipMy{`A<)LI=Uf4i7!0pA|NF zA{W}{DeHcEZt)}}?BlIpRxeUS1I=y!MWl!p{EhzA{~>lm3faj!DEwoVzCO&B~~{an^LB{Ts5a@lJy literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_on_pressed_holo_light.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_btn_toggle_on_pressed_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..f907579a0add8712171c44ae8a44f2abb890f8a7 GIT binary patch literal 1197 zcmeAS@N?(olHy`uVBq!ia0vp^0YIF@!3HFcFAm{mU|=cnba4!+nDh4TS-;nAA_qPi zKi+6-X1I5iQ}x6N2^VGc1Ma3LT()yD|H4?oA{JY6cv*i#fOu_}-ut%Li4IPWmURYn z?mzbVCadG#sd*}ogbz$S=ifD9(Gj_Sh8txkaW;SD@?MuNy-((o5R=1$eA^CRcgBet z3PP+diyAyc1Ui|N0v%MiI$S`4LGLHO+Pryl{dtI>Gr>^py5)-NLQVm>x8v^J zyZ5iIrshxieAh2r6-7m#{(boHL0$1k*!9w|?A*D<>&|bw=OyAAI7vmxH#l+aT77-} zdTU$Tzgr$9HNDuFC+PIUV5U#W_0qFBX4zAM$`1c>6meBpD)RWFh5zLch6C&jKbRTb zXq|p3Y%tSr`FkghsX?c-mxe_>46t~IoV65)R@h#p6KjIg{tJ z_wySayRq)IL$>Zh_kKIZN)E<3#R7*miPxC77xV4;{$TI(3y)VUxvnO{v{&GFi<1Jv zuUG?s1i|*F@?L7^9iov#vT%W>LqG;MmXJj^_)7Y@7V(1;%^F=h1e(-6548&X$qeFt zZyS-l_u8%K?7hY3_*3__9c1ZwJi}Y_XoiW9Yv2i;(=IZ{4H+5?kD2%;pWYM_8v1fu z?!lXMQ#pQq=Q*VX;&zIN+cY3|gZ^|oj9rOx=da2(vM>hU}*dD4j^QQ&;;dD9sU-G@ak zm)^akzI|Kn)@!#a?B^V7zR}6RVDY@>vdJ!;)5k2AOD6xXE^y|kvzs68dzmMVpT}TA zgCfg?sNb7(uAV)6_RFJ3kFGD$aLb&f#_(VEn9uUd?W+qNl=@%A>>V-b^5XNozk zc{;uD+)rR|Ni2T7Yg?d*ssHn$d)4pfG@ti>dTrymqg6YDv@&mRyZ-k#%a`xpzZWN; zseT@_dF$4tEdnoe4OEaktG3V`F!>rIi5r>Py z8+pX0NjAi`C8(b}a4~{?PS*p0BiAHT*f>`Pb=2y7x-IZG=G*=wTOUk)UT|a2w~y!K z_VF}^FSmP8uUO*pqT_<#LS-lSmOc(yK`SNl1Wv45sGs)i^_e?o&K!=nGjE^SEqv$q zn$5iSVpZnG_UX^wPW=4c_;X#_y6Lf74)o=Jxhf-H$+tPYJ1;Wr_yu|KH$OHc91pT? ztJ|D#{AZJ__gjfyu0rIxg&O`L$&8zZU)Hl)*XiHbc%^S)z0Qq~wFmo_mht?pl&iO_ zUGw{J-`Y14zpB#o(x2TvbLa2jJ9iVG6`B23|8eHb>tOj0Ew^X>EP16IcI#tr-9(Ba d0m;!nnHBq$*(#*M?SZL=!PC{xWt~$(696M!`ThU^ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_fastscroll_thumb_pressed_holo.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_fastscroll_thumb_pressed_holo.png new file mode 100755 index 0000000000000000000000000000000000000000..107e10929c6b66575ee124467b8d459a25b56b70 GIT binary patch literal 908 zcmeAS@N?(olHy`uVBq!ia0vp^Q9!(igAGU?%)C{`z`z{r>EaktG3V{wjb71#GRHs0 zceex?i3qBnd=lcNuKx1DYKxLr&LwZTr|{KEY93u#qOo*xaDfmfUyzr;Ek*MX*Dv!_ z>drgvpMUb`W{pW$ib%cLp9tZ+3pO zIr&BI`=%+^ocJgAOv=%#oHJSF@tH^F3tKNVtL)acdzQ9t-kQT9b=$7e)4m!d~pXQfa!jbaSDpQ+Rb)>?U{qCC>TF|NTC6$Gb>E zvP$CHteWX(RbF2)F#5u0v3mXWD+>7s6+<%KsP1A&$-g3fc}2lrw@Kxmai#U&3uiq^ zpVWW%er@k0pRTg)3|e2ho3=)K+AaM*-LqJFW^K;xlW$adtL@gMy0lv7+M}h9i>LiQetvhdNh?=v*oLw>DfTC) zPx))`Yf1H)x?lg*DuT9cZw<6RVCQ#}f9`y>%6k9mo$H<~(dnsjn{H#AdE#Ew9yfcB zlo#?3*6iD0J8_}9r{96fa!bw%&&B$(`yW(pcl;joSgJN4@mVSRmjhAxZS0XOQsAj~O8G_8Zpy|R zn|tZD?8}+7y0FcY%~sx;_k-@8MN!@4y%ZfzmU> p8bI)x+cMDw5V&<>CTj)=>Oa56@|pR_#}1%{44$rjF6*2UngHNvINbmM literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_list_activated_holo.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_list_activated_holo.9.png new file mode 100755 index 0000000000000000000000000000000000000000..a99cea6aca9bddae72f21264a6fdd161fb6c100a GIT binary patch literal 132 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE;=x2KC^h(&L5$^rg=j|I|?ENz_F z*x2~6p8aqn6PqxrgtkP_j1`FjMiQnItk@oTNu@}8gc@Wil=2*zdGVy_gjESw98NLn fw5^;vUy||iZ8p`&3#U7P#xi)i`njxgN@xNA7NaRH literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_list_focused_holo.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_list_focused_holo.9.png new file mode 100755 index 0000000000000000000000000000000000000000..5fbd8ec3dcf437d1dd3f224f152e787992a094b4 GIT binary patch literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE;Ail>WXh(&L5$^rg=j|I|?ENz_F z*x2~6p8c>76Pqxrgto+`87mS6j3i7a6dgFk;3>7a;6(mW#dp2u9h@G>OUbWa&sHyS zNhN4SmL@l|iv8nLr!A5vYi;Y7@{ngvbhTW!XkN-^hKHe?Yri^8WCz;E;OXk;vd$@? F2>>V7Gy(ts literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_list_longpressed_holo.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_list_longpressed_holo.9.png new file mode 100755 index 0000000000000000000000000000000000000000..82036b6b7812efc28368cbd5ea795e18e7618343 GIT binary patch literal 133 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE;=kEe@ch(&L5$^rg=j|I|?ENz_F z*w}dR{{8#y4F_Esd!^0u(iXKjo=CXj@JgaXJ99#=Lg>L2jaLi>(%sarJFIF7WY%Qg fAh9g0E{%o3VlKNX^NhSMpt%g5u6{1-oD!M!3HElju`U;DYhhUcNd2LAh=-f^2tCE&H|6f zVg?3oVGw3ym^DWND9B#o>FdgVmzz~gRdB~7@1;PYrJgR1Ar-gY-r30O6v)HuFx&M; zy^flpsA@>fOZ%61+`86zWG=m36nZN!`v1Q2=v#Kh{W_lYXC^)QdZMwhjacxnD{uGX t3*ld8&t*3u8hqjXT=}%d=VNvBeZy#ppIu(okw6DCc)I$ztaD0e0su+@V>;vk2IlmYm?TMnK`Z868eT+DC(N4);sbAjL-8Q!Ko1In2mR8=^e}cXkbIB)f4|@Vd1-XybZ_8T zfFOw8?2w$p>wf%lJze-)`S!URFGo?jfX3`1s;Mp{lDa(!$*ifCU=FJK?DR7jCy3oe zBVRxT<&3D=CawB5blr3?njqo>bw|~vAR;GW$*>aC*R^$uH1q^@KBBOSBf+vU)NtWg zV>+dkg(9>MQL56LSKBGEmS5bn(p0+|T9Yxt9w!C(t zaLSmv>M($28PoLRYFc|J2miXUt+kh*bs&?2o?Ue{T#sUZ2aI#Kvyg9yy%Ep41}=(P zk!`JNLJMW(1cm?5x}l3{AjQIAiRWdGjm7|o@gS3w0S`DX#BpJ+!?7J!$Vf4X2PuJz zqyb0+0Z4)zOU4Aivk_dI4mN9fNVPQB(KWDc8=Kq_D@ra@k?rPfyVB`^(Xx$fuWUP{ zG!`KhRWmIA?3bq%tqfgb2I?8tHpymwMPnO%R*10x@L`s3#Vr6eAc%X$R#>Jy?`|5BIk5wfrC-e2Uo6Ez2$AdkR z^6W?B_s5%k4_B81Z(dGB?=CGYQu9~#&23&AT$uR~4#+nSpSV2!YW={u+>4*Pj#R&2 zycLb@d8&_h!}!L%C$Hc4A6)shv~mCXyTz_g#NL_JwZ?jQt3#^>wp^tArWJ$n8r2k+9>KLK10iJ~qzr_Vfn<#;e`Q!12q^<0tu8%) zbE(-#fi)Fw1+GLrr=P37t27&FSMVCZmTRQ|B&0*IRy+Zb8W3PDz!q?&==$cxfpkU& zg<0JSl!3)C&GkL`yjXP>kh%i{)M^!-A4s#o(U{KIu9|1nm}Nj2#j%mT18`pef4DB> zHIQ0`O2;0`G9cZ1+Zn74)MWt8USx(%!waL{ipL-ZX&8GSsy0aFO8I*dkmIt~%NdmJ zl$h;10Eb{u%)l4`tDW_`pa3Mm*r~X3C7xz~Wz-&^poF~WK^q42jQ8c7A2l=;bo&*R zg;&R~2<&`#^lf)M_@o9KV*kCTcoc>eRurtVRCuETm))yahqtP({p>ayd?VXqo0u|i zP@mcer|biB;#fL0b}RoKz-L$D>#P-`#5=cDmnxqVTOl@%DP!aOM*v@d^B&#D2Ddb} zo^vXcnBD`|d6$_3hkuhQOZ9V=KrX5bgmlY5PO|N}fJ`|?&;9LKU#IlX`Z2eo)B~pU z^&DrAxn~heKLLCKX8J?uGFQ+YX2O9?pH@Erc!(XnLFK8Ve5{C`rP>4-KnsN30rNd({rimghYILomjGjyosvJ>>Hx5J zC7u>u|FvO)kyl6Omq43ZX4L^hApZI7X&^&~lGiOg%_LH=>s)K%L0wu094$r>zy?)9(QM0*0=`3TvsI>m1E5?__2*jj*xg zgSDh#if<=YdwyL)Tw^WORe0S`(a41|<{kTMWbxZLG0O)K{gN=+ESb$@~1 P00000NkvXXu0mjf?QVOK literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_progress_secondary_holo_light.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_progress_secondary_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..3f03aaf8b3b5e26cd38cb4f733ceed956afb638c GIT binary patch literal 142 zcmeAS@N?(olHy`uVBq!ia0vp^d_Zi(!3HFAKD=TFQlXwMjv*eMZ?A3SJz&7$a1;js#{O9&1tsl>QTD+T4BZ70;|Ltj?fEF-#y85}Sb4q9e042ybMF0Q* literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_progressbar_indeterminate_holo1.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_progressbar_indeterminate_holo1.png new file mode 100755 index 0000000000000000000000000000000000000000..6bda3451da7840fd8fdff25176251b21f343460c GIT binary patch literal 1323 zcmeAS@N?(olHy`uVBq!ia0y~yV3z{24LI0@~*e70>_rg_W6Nh16C6(+4HI?ZHn$jr0k&yTPEKqElF#PaJs z2#di$zS#}JTrks78N#}7W`Q7tm63LV1Hv*f&R~JCW=WbbL0D&bq!_`hhBUTbh>(LZ z+_;mc@vF%>aAo_ z_dVflSR9O@D)YjRtQ#*DtXnJg#yh6CP_u5`-TGBE`uPkpQ+IJP^!Ti2a?sBb(4KAk zU;q35qpuSeANDv>+wtkb@=~pR`cr>SGH2-Xxy!=fwk(`+fw*bM)U)&cFaPb|eC(6x zDn3Q)eIN9rH-6RJ71ssfB$tcLHWY}A<34U ztsPz!k7i9Q;rOOLU)wp}&Q%-ejEQK@czW-<23zmeURR45t5@3|+I>Il)Je;AH*a>I zpYl+Dvl>uGFPe_^-}WzeOpZElcJx-(L(BVNMSaKjJhAiwyWJ1X?YoyteEyp+`0>5@ z-^FVlK7X72lkZ{2Z_jG3{d*WU7?lb$NX(35ZHT*~5N1~V@4xZQ=dn+dcP;&M-tOO{ zeGCaMuhbX}jP`Od>|NowCg;_k>t;NRNO z&v*AfF+9oM!OXx=KNH3ER{ya?J#tE#SpHV9P%|tj?!LSoFGF@9as&rIuip_bb+Y8! z4;vQ2{&s~}@9&HUPF?Y3NJyy?WvB{lj<|W{r+LPc*ylfZ%5@)9$IPE&d-ok{!_<|t z84jHIqQ&rK5!=R1D?jgdJ#xO|$BwmcxB1;{`n2(m?Ycxi?fKiTtG#_G;BrEm;rMc7 zKLKNP-}f(HWdxtU{pPzX;&zYo&TXFp@BR^Z(*E$~@8-+t=Z~KM#CAXs#gdz!{}=s# zy5OV8vVT{mGM;aq;=l29e2~;f$#2CUBP*F7tUz(~o1g37{+*PsHNWK9>_UUwn%1204Ej zs`LL}{=3}q@#^o}tfpjFi@y4l{q|qcu75gzK0cCSIOBwtV5a}RuQj`Qy~gHGI@T|% z=IK{5Czzt7f^UD~mDcUI*kq<_`gZxVxd--H8DR7MhW~F@n;$X<=BFQ4$Zo%;`(N!3 gM&?IO0n{i<{+I2a9B6nfSsY}Or>mdKI;Vst0C#UPZ~y=R literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_progressbar_indeterminate_holo2.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_progressbar_indeterminate_holo2.png new file mode 100755 index 0000000000000000000000000000000000000000..43b379e58f1884429cdec65a911b95bf5cbdbd39 GIT binary patch literal 1627 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 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_progressbar_indeterminate_holo5.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_progressbar_indeterminate_holo5.png new file mode 100755 index 0000000000000000000000000000000000000000..fa4697be1b3f40dde64ae758527f1e3e6615249c GIT binary patch literal 1797 zcmb7FX;e~a82&61%Tc8Kf5X=tXP z<3hGMG%1=XO@mvR8A^J@ki;_gG|DBhxkzVz%)joBd!BRddEf8-p7(j4o4nW4&2Rx~ z0RX^|w%I&Ki=>ND+I|Vg{%cpNZXC!rvG78x zyIytZShQ^=UQ7W`s>(7&OY^}KdcD#K-%Y{}4=2V_P;os*?xGmEbr?JSGujWAfyqD9{y3(k{a{1`x%jw!mf%I7k`{iUsE}Fc3s!7w@k;P zbTOJdaJ^~HBD78d=aLtoQN|paFJ^geKlU}ati3#SAeb&bQ|GLbEkBDZFl1aRX}IsF zZpwf(r5uaP0wQEx<3d7%a_Kf@ntsI3fVt}IvQU>je5_Da6E0w2A?)a9r_=J~+Z;_e zR0>Mz@Tr33)>tyU>>dXA6t**W%gMan`3J&vK^cuT2Pm)Ao!{RYIpy7Amg2wJ^Fpl; zA3oeBBBXhla!ie6$cyM-Zm<#?Wwe4QP=8%-hVM`(w()jOPm9Vb;J?G&rxz6%g4Wv@ zUS_3^)Ak-l?sXpmDQs(Z1HCgK2p=eQz&LR^6-IsgiXkK3^CKS2;>fQ>L*^B;(>Sb5 z-c=SjmAPuLqTJek4iMhL@OC%80g6dYqYCD=*JsA?`8aXf+Sb;#{U>v#MmSxn3$qvseiW}UXTz~!JrH{pUc#8MsYi6Mv6p#% z|M)tLvmSFjIV6uY@RJEhsJ*mJD-?+^oPMDp$Py-~z%D20 z-J{FQ2lpOVmwk{Luzl&y>FA#HyNHiQml+r=dA=hR0drG0N@~*7tbl78sScbH95=kg zFNlmSd3-u=bm4Qv^Hj#9_p5*Aip-waYCKzY5Irjmk!GX2DE+W9A~am$(c{o4&XP#X zQ1?C{bGYv^o|!8j*cKUX^qa-;$ zZ-}0o5TX(9>+XgC2AOb_Usi1&hHXpEZH20$+~dftth#K}FMX$Fz7+pzw{S!Pr7t>^ z&lINL<@eSNmilJXY3Aq}7oey-R%iA81ZYOWMk-wFHRiG>B&PIpsmeB}=Rz%nwCHuD zc34D&ND2@wE%r+^!WD{Z8w4M>kIibf6xOTO#Y{Ny+gPAq=f(7K=(&u)k zIw>h4k1^Gpgt*eSh$?2`dU$VY%Hal&?Cd!+`;X{?01XL@hS_w}28e?RDr5MdEG%)& zP-vmy%eI$zd4%JUHIMb0Z;q^ZThSJEVccVcFLnjA7yr+X@h>&kV_8F)ukKOv(nQ=7 zQwaAD3Nq~9Mqws$SQ}UwjTcYy_Z++BZD8y`XB#wsF4|x{)b9r Rilycxl8dMF{hhR5{{=tB4?O?? literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_progressbar_indeterminate_holo6.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_progressbar_indeterminate_holo6.png new file mode 100755 index 0000000000000000000000000000000000000000..36d1bed07172c00a55a9b6956bf15419bb203b80 GIT binary patch literal 1940 zcmb7FdpOhkAO8#yB2t>mP=3-Om*uQtwQi&0pyXDQFtu!ktm9UgbIK{X#3?+POCdj( z8M)ua>0r(wwv}?e%&;IPY{uMM+x; z0HAX2EY<}81#JjhZk31r`|M&7&;jlo_M~eBZR-6HS!AaM*ou4V6AOM`NF~l`7?>5L zRX@UTPx3A0jP&O^MVz#^_Rk|2OS;rYKl~Yw9PP!#7sRYAr$F8$-{irmFWaRMjOgF9 zlft~LQ-)H+c6_ETMUyYsTczmM#T#-`)cD=^GE($@r#9>xq6R4?Nsmq({;#_s-|MY1 z{_O$zt){hLDDsv#y$VJ>!}rSxXqEv3Z+8C;{*I&W8Q z8SHf#_O{Whq*cwY7e)g>5o?uVGx0KQEejo%@eN}iif8%6O`c+3P)RDnuVHW{A?`AT z*nHe==s&ul80 zfafA3Qz*3LkKv(3zmD5P1;T2%PxsTtXbcAhIEQ(*K<~)oDKm2g^?fR!jUL4^5oCfH zPl3b z

inUCrId`LZzkH21U)$GbukM~(inLp_#dO&IXtW4*ketWdBUi=SiF0G!YF?9kN! zwS^Y($PF#^*4So-a++H8p%x+8`150kgiNfhi z`*lE@6+OwV2MP1RNs?^f z$IF7rfr<>Cr;!y)jyg5@(03c9hIZ$`0ClCr%}}Gv$)WE9+009}f>C4P-zTdCsD#V7 zNlVC<{1LKU^w?j+gWPTKLy^FL0c}iZBGcXDZcbSpn4ff&GAAz= zq95b)_w$Wm+rX$+;%RBe9fCykS_}OJ>&Wt3xpY1E?d|7H&g;kj{%naB5Z3&G$3k|_}LZ1*k_ z0LUpux&I-l3tCc_())u^uTcg}Vt?08ZFCE3?xzK0Z%TeP2BX}@gs4sp{_7uYpp8{A zt8RJZ|M==!gI!p{{Wse;zp5X%Yae2Ttqi^(>NqkHU*W>hCsXhYWB+zSv+Ar?cbyuJ zO0St)@)8G4Ocm11f@Q&SV|bY#6!16!u~ne zF>=IG=C1==yKQ_#O`gs-gM<^k0)NY*&rZ37&@lf;pWq6vqI}#0n(QW{ z;N7gwkuGc8usjg^LLq)rqR?jtcI#47D`U*aE#9U*yLVv|Im1n%*Ru^o2K{l`dv4f@ z&tBd&Q`n^Skjr_i%u~m+G<}rmiYG)S9XtcS1cvN`VjIlih(~oWs@`&<8xi7Oh)y&z zG2iV(;I8gZ_zqV@AL;HQ(<6BLaDSI-|8Ey$YOd4gAJiXJ6vl)rK?(Eekxh$Ldkw+N z4d`-(1$5cBoSrdIsI2s93BfTpPli;AA}Eew24c}i7`hdK#a02?)YbXblJo{AW6dR4 zNDFBnjI#csvywOav{?l#)Dy~fNK#yui0x%^%Wy0i$4H^?(!5&PGU958Zol^YZ!Z6e zWn?TuqR&jy+!7ZfQOPuj=-*Vbp5Vx~`B2nBi@e{RXWM+;sCM9(ajy%z1Cqr$fjFlL zS`|Tyla=;&XO+ML8+t~spC^jWuURa`N7OHGXFCSMnEIiIzzp5W8%B+maEQN_$T6vq l(xXjw|Npjj{{JoZRrWXX_^D=H6k77sIa?=e`6-{D{sRT4bBX`} literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_progressbar_indeterminate_holo7.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_progressbar_indeterminate_holo7.png new file mode 100755 index 0000000000000000000000000000000000000000..d3ce701d79994391556b8bc4c1c28ab2b05c36b3 GIT binary patch literal 1741 zcmb_dc{tQ*9DkAHaU}F4lVhWdvX11+U1V~O2t|ymM2sypu3^TOuwrr_6VrjUn;dD2 zC&olO!#YM&gc(O;orTO8jB|gq&$GMFvw!yf@jjnF-tYJQzMt>s^LdS zlpnZOCq84$ zN)KWzr{sZaymp&B@O#o34E)YmoHD;li_12sCifV-hp`;2&Ag|Zom1j(?)0m~HO!ssLq?KNkCTlKhHfN}N zeB%KMq!e`LGr0EiC0ROvVp>QClzZ6Qu@&=@TA|tGo&wzM8IP#dVvY#%KvTNERnF02 zemM#hNGuvo$bVNBHqaU*3i|ILCXUPWb%4*=e}|&8)m1G5q8Z0VLh?R|GmoI9Ml#lY z6TXal5@+P`ftuqEM6g zSr{QCe)7uLwQF5ZEC>1{0!_R0%0w)2=q8Ipt|4*D+fSd$<&;VDB9|H$gH7HlbnN2` zg2t9Xz9T(W0dJm7a3Rz#6!b-NG}F|U%$4PP8;ep_mU-2)_Y$z1ot7O&kFcuen?cPR zlFWjp_CS`efyyzgQ7=7ZWvy3d5AdV1uv93KS{9VZQk6}r@5S7ul}_|F@-8WMAXuI_ z`oI_;F(^Qh4M}i}M_D%ufcKZt7J*HY6d8yO1ny?Qs!~$Z;|$u>xUgy2uBQ$(UH5#D?HM%U;dHMtbM)=T-L=1d{n-{aQt=%;mRTR4ImJ(^mcWO*UD*X* zp#LF|b^>jnJhs60;)bfS5jI#Xx@!_+Z5`_B zh03*SXW8Vzr;wvGaQP7=1-I<`n;7EIhu;q)IM_@rK&kt$Jo%W?)oyl?r|*>;&laxn z6Y~r=`(Gg$a<4;3P)V_<_IzLO6-!Q7CgjVy3pzqOzT8i+t_#nayWwT$6opAM-Q3Mj z%=L(K$em-Pj;1gZ^Gn1sdUOE7y{JBCc(x!I`9=$=I%i2fsHJZUjg!#^0pckX?YW`ewMPt0{EypUlII4s3u+IWRY;jn+@mX7cNdu2h>En-zPpnft65dWy7yB zV6>1NXCdeBEJRt_9vBPJ5zowvs@G*B9z$o)&xAq0P$UfO-?rRP22IP;MPNs^c%!qB hIChrDwoJYyT2n!p(55D`Dt5jWyVK4#Rj2%Z{0mad4D|p2 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_progressbar_indeterminate_holo8.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_progressbar_indeterminate_holo8.png new file mode 100755 index 0000000000000000000000000000000000000000..f475f2def8587e4082a511b47fe9a143ef1b6a1f GIT binary patch literal 1744 zcmb_c`#;lr9R6e#xy3;j4$}Q{s1>@b+>T4L8jT`_q~kg=!^A`?ahRR7+%M~;t*J!0 z4CT@}&fJcBr;L>{jHa!P&Wz!FcbxO;`~iJ__`aT>p6C01Kkw&BaYj3+DeEc&0MwAj zZ7~2S>Oy#*(rW0t-9BC)fYn(@TWiXLuB&jJ{H246U*PiseqNs=#0xZ zMu-zH$^e_(e|^#07bL7B67sc%L4tp5bDbv|)>NG0?d&ha^Tub@ae)_5YGeGqs~w2w zKr3Ecs4D2mg`20~fK}P(0Wfo1mUtw#2yxJ?6JN`M5QP&h1Fn4?qC-u|w)Jgb8 z(se@iY(24joKUR`1K3h+ZqjJQOk^QeKB>z8ly@zJ)a*9R)HUq2=hm7>+O=t9H6U2E zmbaz|V(UCfBOEln{uznN_Z`Zn+EKJBO^x)JlA825<81+;$)dE=|1o<-z_r;`k&gHs zrEg{|2;)JR-=f7K9J($#)=@u3Hel}yj?>~0f7QZ)6$+v{Z*m_gLB-SC*seq^F65GX zUUwSAGLQkW(8tjp6>UO=^od}>==x1WgJGl1i|lfrqbxGHNLmrUrG1AF}lhrjN#h7jjzZRuCU zBj14~c-@C%1@oI&YJh5nlBSs>6K#Pbygq;EbbfgRdbU7g6=Z}E>WJ_5q$brE)JTy75qgF1jsQj$s|phy zwR=ht^kiTx4pfCQ|J6kD%`~UY@=SQ?He3re+{PE0V|Pj)T(SfcziYg~J)z{&<^iar zQ@TgxPLkvvgerGPCZhy9O$Gh%ai?eMIO#|)S8He$kYJ$P#xTF`_$wiu?;PWj9;p!kd zDOQ}L1Z}h02-+sCwj#nIY%PAru?7(jzB&C?%_7%1s2n+5D7(#&zSl=Y3oyP>-RZD9}lDL#7tE zG2T$pMR-K>m{kFh-_d8dm^^8nwFXIWzls=87Zq*9}9*q$h4bYD3V(U3*>y`;kQ& zOhv24Ou@>K=p4kLP?TrphtpR+BX^U$c%&dWzj)lw!c dqo$y5jh0~ALG(3}*8_aH0@4m``^4r#;@^=84!{5a literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_scrubber_control_disabled_holo.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_scrubber_control_disabled_holo.png new file mode 100755 index 0000000000000000000000000000000000000000..8d99da5d3f9a68752d5d2baa3acced48c4cfb179 GIT binary patch literal 1409 zcmV-{1%CR8P) z&1)M+7>A!)LN38JCYWG^5ylu(f(fRC9!gKW_0<0$$6SMMb1@NVB>O-II0H_E5PvzQ=apOtAf=o`8NGord`QlKHcE_}zHaDOXU&kL~MC z>7zvGgv=zU08+|2u*1I(Ohw}ccq4>336()G0i=`-;6bSLGv*H=L_0*nL0YD6`DHOa zzyn<;?|2tL`}`f>>E?hPT{o|I6<}nvQ&gWni8V1kc&Uax6RaY-~5kOBbtoul-Agt>;JL4>X?vC3Y%2G78eYheYJz#^`a6KQhgsQs} zP6Fup{7u(F6qrqI18ZD+K!b9RdZtmu@w?Aj0PX)~x%Cg(aQQB9=>c`JUq;L$HIyx^ za1mh5;oh=gZ3`;_^wtsY^CAUiMcc#}+a9oLd0&~aaznNP+_Su|%vgB@TLD6Eo+=_% zHZc}JO1aEzkNw9{)wVIhrU%U8w8L#NwvA0oUa+{w8@O7wa*H$HulPi~zmL ze6@A)=oyn|ARDcVN6&hd`B>jknFtVN`a!#$#w{*w><*gWhc3?;j335q1c=qTzA?D= z+hC=bjIlj!1cz;od3$iEJN=X;gblhuoppa&|ZU~C(kF@4mN=e5Cg z{|Dqt;D2tWHn@&ZqW@rQ8=D@G>!bgX2`_+-y%2N8F|YJ(;!@Z?RK@hOdBw^mNdSLj zDnP6!%{*ge6Dt9P5Ix}3=H8NWs%>J7%f#AyhkMJ0wJlr(_<#!eGVAnEwy?rl03FOC zN{E-7@y;c%hwI$_F|*wZdMF!M<0OENGCX26*K=@~-ziK0=6d$|u6m_~*V84zkHg2fEv*93tER`=NsOg$~+R)QE{g zf=5owB5dU{@D;^7)ZHVH+ZiRRz^yglONw*0xPx*#!AEqN#*UU!ZlU5!P) z&uS!96o)^X5W)~b2qA+}M&OfNvo^a8X@FF?!m0<=soK+CMPoq-5qZsym%vr=n+g!_w;H0_5FotE?Y52>b_X&1J*pSJp&-f_5#b6rM}!(N z5yA=r4U5pE0J98ljlRQdL)JO(X0h!d0-Q5jj4(X8(u4pu+*qTl;rD=Ds^95#*#HiJ zec%i@Wg>(XLvaMW{Vs6Cau{FPK=Qm>?Ata-R-5JY02_8R6Eb04wOkcrW=g`*q-lohyptcLNGeWV~hDZXnrhVCa*SAa6(@3OpM zXCC(vqn|M`dTkMMd4R@!t?jHThe-G->rqXjjsRtM{~@bA%sD`=@tWy=BS;jYjB>o`aN=FF6O?_JU}8*6{!pV4v=E ztTlp(+|%3$oJ%z1RW1A#dYu4fq=0Hp@LG_Nh!t2-=J|E`=}gsoriC zX?tO9;~IiIdpxy@>BYOi$ByZ5;1Ghsg2hvSPaM;3;v80hN<;1u|l$k_!87z5woTh+Bw)+>_|f^HC{J$a4{Azoxi>k=8_{3}CRi_Q}+{IQUf z5Hu!_VHWWW_yVaNrSh^!<7t*0&F7qiCud>?kgW&k@-N+X zEpRPrkMOCPg~ZN`vkM+w(9Gm4km-hxk>_?FA~WFDnT#R4M5=&Kk($(thVrFckPv<# z%>z_~b7W?4lk-FWEw^*fopsAzwdEnI{a3@5}B9ub5umIGgbyZ%3M4M@?53JSl!##)X{5NX!qz zPL|lOdOzy z1oe!UbZ_x)Mf-Nj-YeiXDcN%{#flIP=Deo;Z)GnHS zfrHK4ycamwyt~oFaH72cdso>mj8ptxVG?rZUSXo>Md?Z+H%yjy86mR@PDy`>&%Er(@tM}TcaKEh!$<%C002ovPDHLkV1n%=n5qB( literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_scrubber_control_normal_holo.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_scrubber_control_normal_holo.png new file mode 100755 index 0000000000000000000000000000000000000000..ee55c8a16c892c83c31a4678d9882dc6f062be13 GIT binary patch literal 2414 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 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_scrubber_primary_holo.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_scrubber_primary_holo.9.png new file mode 100755 index 0000000000000000000000000000000000000000..03a5711ef354ee1d34a4bb4695299867d1806d81 GIT binary patch literal 143 zcmeAS@N?(olHy`uVBq!ia0vp^3P7yJ!3HFkD$jfgq{2L1977^n-`+UL%b>u)a`5OX z-Gl4CD%#HJtd^heB#KCTB6XR&e&kwfS!c+QQ)J>gTe~DWM4f2fr=+ literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_scrubber_track_holo_light.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_scrubber_track_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..1a6f577fcb9e0c21b07a4f0b68c9a91fcf97c0f4 GIT binary patch literal 1095 zcmbVLJ!lhA9KRZ^T2gTktq$_MYIQKV_by2FWn4{lI24AQY5H+Btvyn}f8E&8+AGdFm@8nN1 zEs8p&*xIy-Es|9dEd3MK4P8njIUbG5f}rrQKaNmbK$)b11jO?Zo{#cvj-9Y#MouPE zNm=A$X@t^9M6#&FlW`FVFh*7_@gY}GSo1mFr$=E?3igAdL5QV~4+#-TvDw7h@k^U4f zMj~trtN%~VIO+`NH^)EC(!Qb{=x?`MpB}b{2U~PvTsj*0%f>-ER@JPMEY{Y(K1v>% z1HrwG<=1P%MrHKw$uo~P7hb;kk@~O_z1VR&z{rOy=K8zkPkWdz!O-$j@W{iXZ-cAd zJx_nm%v~E}0zK=mR?kJw3g16Z*8`0gzx#x_&812B;Qjvl9l^1==kJ}aHcz`)q*>EaktG3V_aL%$;iB5V&i zAGOTMdVg@?i=7@C3w|9{$~D_|pW8T9$NikTcwtwLO!ofbTI&k?)i?NlpMK>U;yu9w z1IF(v5BNQOr?ZkH1fDr_?sWf|nF#-EEvBj`F(c#&_aAKy!1(_!~JQ+MjOw6;IdJZ+TDJUB*kUWvWlfh!d%si{%%%KK0 z1!Kbn7AJCeGakpl!H7;zSmY21^hlb5}#i!3HLUsRjv#9;Q4I96^H2 zVvQk38(0;l8aCu=c@**_JmP!Vz&P1qSHc0734Rg=5=*4ncvxO`Fg82vN;<$W;haQ) zkw-aC!*%7!-=yX1dFth2ygR4K|6g11Z}QLSES;Fp>_=UDeZ{52&Y!J6#M~BNI4?tB U2g}<)V7xGRy85}Sb4q9e08AjKY5)KL literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_spinner_disabled_holo_light.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_spinner_disabled_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..4cef09547fddc1e6eda324e79577c8587ff3ffd8 GIT binary patch literal 1326 zcmbVMZD<>19KW<%JF-+#9V{CR9GIJbJ!s$h_7Slb8XW4q3nYP_q^Tj z_xry-cW!9l(GKTfCqWP$nY56@vw+{?zBW8=+@5|NPlv43h&8N@TUE(GM7*pO5y&Xg z1j-?)JahU}6d{P#aXCL?jbxwTOPb=7Y>cm_=$K6qk^Y)4m8OsdifBSsqvX%;uaiJ7 zN6C}HEX?XjG%2TN4KzGEkT1&%CjS{ZM zcvq8*b9aA3HV}Km4;nHqic}G_(zJq9Db$z%TnZL2@_g$Y)SlG&BWl=9iauu^)^F zIX0dMr5MI<$IUSb$ig6mY=}!GQX#UzE&rdKQP>&EZjS#nOY;bKpuOB~eZ1MeJV?bi z#=uwO((0W6K{&=TLOfr){?iS~c^0}BrJk{3-}8U0fAR9o<9Dxg-2U)-;IEJG-aByo zRwupi7W($xjml%k`VU?;U&}<^*xRk#S!mnijuDRL5QvRE&D_h5h9BBGs~#r`vClBENFUb1^nsxZv1Wc%|jS!;6u$ z=91m}U8C+ESH}Igi*rx8j;(Zh7_bVvV`6c>{m@IDhd*iU>Rmc0rrm?fI{4mmK1LSi z9p~mcTe=@y>`Mn+Cs&p{DmX#^-dp(Ko%OZ8vp22Jjuu?4CqDAD)aRuK>Wh-Iz9hBP zUyxer%U>N4j;y{ne0kxYp1;q#UOn^UT5uUXBf~eH`@pkAOJm4^_Wm!qr>c77yNUGR fwI}x<9dxu2Km2;&s&V-gu>WA0)PS&-D9rr}Lb0Na literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_spinner_focused_holo_light.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_spinner_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..4b14abc6860499140e7749da82bc14bf4848b1a5 GIT binary patch literal 669 zcmeAS@N?(olHy`uVBq!ia0vp^EJ}aHcz`&H^>EaktG3V`FLqBOpk@k=I z;p+svWM>sLb7;nTP0>^LY}ClGV6t`fo5c5&EwhICl0cx6OY01g?3|Ag!bWe-Rd4?N zpG)Q1jjG?tEh*k-OrA{L7L#ujukYiZr!aRTvka4bW9{_LQ>C0bjq}fHmNRf(l2V!b zkI@yE^wE~uQW%$~IQkXMGYOp_{Gj-p@N$kx zy^NQcD}~v7xSyPrFp!<#XpkVK(46S-SD`t{;jTh+vcq15#R(2`6&5Et)G91aa)?z} zoa|t$;A*gdZ$grUfcz2-wj*qseT;>UF$M~19nx$^m|MVo4zltW6%A!ZQA9nHPR(r3aYu&KiYu8OQ_?e zX?;j~>o<-fe|x|C_l^d?l{4G_SNpi*q7qi=2}>U<1V$ZVd&0Ei>+GX@qph0xQcA0i zS>8P;Sp53qh92t|_I^*fKX6vW*KJ$$m$8mf{Cr=q=l`AhpSf>mx815Ve!Oqi{yp0R VJ}aHcz`*40>EaktG3V{wja^p)B-$RD zCr)tOc85FijZThk&ZRfH6^7SNhuv$*Y+d_iFVE_&!WUIl#EfSe%(Oq`>%K9F#eRu86J(AUbRyq6= zXAWaAVr5&ylA^^E!FXbd#01L|S0pAFo{*84pm`!jVuIp{IT8~jPu!81Ab3JZVgl!h zAc+Z#CniY<$fRWQaPUlGVs2_$a;Tv}LDNt{!Xt}^gGGpuxv9bBU;~4~L<0pw4-*~_ z4k1Bip+=XZ4J-;14I_T-|9{Z1fi>KNPhx|Bhn~a+1`j_817nXo2?J%1J_!S1k986T z%pT_?3=BQqNf;CbpZKnCSGVg+ocG7nKNc^&|9!QOS{!!b_WAYkzP27MR{7IER)Yc& z6f{?ww|Fk^2;^q=m|QSH!_=U`MpZ_|%io}(Ci(Vs-_r*lFcj5P9aFyT@=|`<#U}aX vpPnCIS@M5}{^$5@*Lk*ne%A3>tBirwvdE{op)rYr_eS3j3^P6Xd-5qpit=Uy!SS{vop;acV~9ZN+!n5VwWn`$;{jAG?{sm znV0M&5-X;lg4#nxFG4ONc+i7hN?U40p%p~XgGv$c-$AQIi6>IX?IC*TgPHff zH{b8~{hy^wmDs+8J-b$|Wbf0}O#$21(|m;vkt(elKIXC(8OD6k zF-C9+8`jkL4cx3h*fyhly@Ul0M-PfItLDgeV3jC4m$G_{I{LHQ%Z#C9Tk~ z#iWc-qtsJGv0kso>xnq=M?@&ga>Rj9jA_JzDVG{y%niDl3>pqh-}b0YTs~qnhRFoY z2+Y&P5FAg}mx$e3T5T-#okw`{?Ek{J^D4EsL+^U}`-%IX&)u^-=05sq!*`P% z*H3n@eaAYm`eFOc+wp#1G u@a*`T@u$x^8FqF@OXtj~8S>@Z*w5Urd-`s-_p3*ue@8Jtpk2)EJM<50PbLsfV5}jv*Cu-rm{B+iW1<5Ga4D z;^tv(1E;VLayQRsDj!&KNd41)?S%*8)2&ViK6}yp-0`~ig3BP2E-iIdG8Jnkk zJUC~6+dPX&4^3Wt*pfVT<^^GQqdCV4qz-5DEH}i>IM_D-9&>R~|EasIWsh!rxEr=S qCaZm}h1s9$E067-pDO>YmSN*xq1|T<77779%i!ti=d#Wzp$P!O8h_FN literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_switch_bg_holo_light.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_switch_bg_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..4532035e552f45e97833184994dc8fb22ebb36cd GIT binary patch literal 1186 zcmbVMO=#3u9M4jB-MUmzWY^vG5K{{Z&g8vJXEF($md<2G2b*@74qAjo)8x%GPLr1= zuRCcK3+p2EVA(zGK`$Pv7NmZlf~aVrDxO4TKT#PJ+=Ig0c0uUsOFL8d(CuL#B=7xS z{=eV^Ne_^ITqG6EMNGGW2N9dK zv=YiA%@`YbfI1kasc04k$bi}->(~lwflk=B9ExU`jxOKP^fDssAS#)5f~(!Q&9SDD z;PypTpgJiuWbPh!QU7>vK_4&clEHO#vK_um2`of3*0+XjPxcesBCkx(!8OmZiz=j? z;Fg>kP;+bwyNHd3K}ZKNz{X%0M%qPD+{p?60v-rF6hcsxBZ3S7TVEWt<{CvguVm`B zsFdJ_2ytYduT(1GN+gWk5)UOw3OEEIL^VR*m`yZ4WP7a*1_gP#YdXZlHXAT%gLsrA zIO^$A2$rL&%fz-mU)36cU# z3GoKUO4ziN5hIzXD8*xG2-7gGq@=Wxq-aqBf|PDxv$jVxTSpDMCbe6~##Y73DHmx3 zy9JDg8y%1v!UTIm*kM!sQC8J-(+=dIJWJ6k$Th!3M#jY!yO>|uT*1BoA`u}ah=Nj& zTZEZ(BqJna-Km%ug3^p z4o=PQI$oREvt`SXod0nD^!(i1$(z^J<^%689ekZ%yUOY+cP^|ge!XGg&BFJ!m(^Vm<^TWy literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_switch_thumb_activated_holo_light.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_switch_thumb_activated_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..9e497b3b442baaf859a07463143638d915d2cf64 GIT binary patch literal 910 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 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_text_select_handle_middle.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_text_select_handle_middle.png new file mode 100755 index 0000000000000000000000000000000000000000..ded82b4be4c26c9f78f3a05092718ca740d5da3c GIT binary patch literal 2845 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 JS!wJR`!DBpz#;$u literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_textfield_activated_holo_light.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_textfield_activated_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..f1d0167cb3e8beb8956045b141c0ad9e6edc730e GIT binary patch literal 329 zcmeAS@N?(olHy`uVBq!ia0vp^0YIF@!3HFcFAm{mU|{OX9@}egNMP>)z4*} HQ$iB})K^}) literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_textfield_disabled_focused_holo_light.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_textfield_disabled_focused_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..d157d7d60a5a315c0768e6974691821c77df98c5 GIT binary patch literal 464 zcmeAS@N?(olHy`uVBq!ia0vp^0YIF@!3HFcFAm`bQfx`y?k)`fL2$v|<&%LToCO|{ z#S9GGLLkg|>2BR0pdfpRr>`sfU2Ya}CX1P~bN(?fFvfbiIEGZ*dVBY7UXy`D!^8fh zO?MCYmH#i_q2u_PQ|=8zrlw!rJaa|0*~Q;HeHB-fY&`qv$obtD3Qq}ICT4IeI5aRY za)C%y&q*qtR*rsJTiFCad{z+2!~rB3SvUk1Ffg%j1TZi%v1kA_Fp0pmePuUDHed#e z=dW0O6KyFVvf_WYfM->m+d?i)OK_1Lr5xFlcu_p8SEnV z{^|A?`b-YQeqZ%>o4+yGH7mr|AG*(8_`{Idye#L(3*{INE%zhuFG*GyA794nrRP=p z^!uF!DQtY|$E2UdwXXc&*f#mm*}mOhw>%RR?|F22)=lZnv$oc9u64EqdTZ6kh#hH` l@BXEWKg$)*ShsZtv(*`D9y2k(j literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxhdpi/flocktheme_textfield_disabled_holo_light.9.png b/flock/src/main/res/drawable-xxhdpi/flocktheme_textfield_disabled_holo_light.9.png new file mode 100755 index 0000000000000000000000000000000000000000..c91f7da91e4028574774f32553c040a1b058aad8 GIT binary patch literal 315 zcmeAS@N?(olHy`uVBq!ia0vp^0YIF@!3HFcFAm`bQfx`y?k)`fL2$v|<&%LToCO|{ z#S9GGLLkg|>2BR0pdfpRr>`sfU2Ya}CUud?i6EUPJY5_^DsH{KYsl50AklE~{tL0U zcmEfi-FD@|g@mTCef&{O?Q&f%Et>Zt_wAhT^k38c38!-`10xd)M*xV_@SLRLd5PiY ziV$gs2B3flh~!cLlB@zil8FOIGO}<8EMS1^04ck|YLINe4AS55E2hqDOAC;>;+yVq z(?6eQG)^;sX|K8vV_EmN^KcrQ*WsrDTD##AOgEO7?3*OQlTqkVw4HgDw{BVEKYmsA TOP>UQeq!)+^>bP0l+XkKEaktG3V`FTff5wG7ca2 zujVREc(N$R*2(WAy z>a)Y*bd(-lsV%!4bw1tT!Ti*?*H>gWdlsKrzJ=*{OpnawDbGG@vfB9VcrYb-+NB!1 zr46<3_m{8Nd>#}0`sWf||1xoqUuQiPuiJOzw#@UpO5t#CJAKjnb1_Ba_3t@V3$z8a z)*h(-BmXlnEUx$Wh1p9*E3z#n?0uCR@Ht>r_Tz(ZE+#Y|Y`A;ul(D-`o}OsF5{T)x z=9bFh+q3%9u53&D#=lu&e$I^sSO_ldDBS*d%NgfM;nfbGZ;0973ihjeczHWNFg_VP MUHx3vIVCg!05k*62><{9 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxxhdpi/flock_actionbar_icon.png b/flock/src/main/res/drawable-xxxhdpi/flock_actionbar_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0ca0dd9f6804f0f348d57e31a64c5375e2d3b5c6 GIT binary patch literal 7346 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 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable-xxxhdpi/flock_icon.png b/flock/src/main/res/drawable-xxxhdpi/flock_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1f3d304639276afbc5a2c7a0a2d064f52e908c6b GIT binary patch literal 28591 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 literal 0 HcmV?d00001 diff --git a/flock/src/main/res/drawable/conversation_item_received_triangle_shape_grey.xml b/flock/src/main/res/drawable/conversation_item_received_triangle_shape_grey.xml new file mode 100644 index 0000000..cc4a1f1 --- /dev/null +++ b/flock/src/main/res/drawable/conversation_item_received_triangle_shape_grey.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/flock/src/main/res/drawable/conversation_item_received_triangle_shape_orange.xml b/flock/src/main/res/drawable/conversation_item_received_triangle_shape_orange.xml new file mode 100644 index 0000000..d8b2bb9 --- /dev/null +++ b/flock/src/main/res/drawable/conversation_item_received_triangle_shape_orange.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/flock/src/main/res/drawable/finish_background.xml b/flock/src/main/res/drawable/finish_background.xml new file mode 100644 index 0000000..cc7955f --- /dev/null +++ b/flock/src/main/res/drawable/finish_background.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/flock/src/main/res/drawable/flock_gradient.xml b/flock/src/main/res/drawable/flock_gradient.xml new file mode 100644 index 0000000..58ddf3c --- /dev/null +++ b/flock/src/main/res/drawable/flock_gradient.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/flock/src/main/res/drawable/flocktheme_activated_background_holo_light.xml b/flock/src/main/res/drawable/flocktheme_activated_background_holo_light.xml new file mode 100755 index 0000000..9503d36 --- /dev/null +++ b/flock/src/main/res/drawable/flocktheme_activated_background_holo_light.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/flock/src/main/res/drawable/flocktheme_btn_check_holo_light.xml b/flock/src/main/res/drawable/flocktheme_btn_check_holo_light.xml new file mode 100755 index 0000000..436cc94 --- /dev/null +++ b/flock/src/main/res/drawable/flocktheme_btn_check_holo_light.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flock/src/main/res/drawable/flocktheme_btn_default_holo_light.xml b/flock/src/main/res/drawable/flocktheme_btn_default_holo_light.xml new file mode 100755 index 0000000..ae14dd2 --- /dev/null +++ b/flock/src/main/res/drawable/flocktheme_btn_default_holo_light.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + diff --git a/flock/src/main/res/drawable/flocktheme_btn_radio_holo_light.xml b/flock/src/main/res/drawable/flocktheme_btn_radio_holo_light.xml new file mode 100755 index 0000000..41eb6b9 --- /dev/null +++ b/flock/src/main/res/drawable/flocktheme_btn_radio_holo_light.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flock/src/main/res/drawable/flocktheme_btn_toggle_holo_light.xml b/flock/src/main/res/drawable/flocktheme_btn_toggle_holo_light.xml new file mode 100755 index 0000000..9dfa858 --- /dev/null +++ b/flock/src/main/res/drawable/flocktheme_btn_toggle_holo_light.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/flock/src/main/res/drawable/flocktheme_edit_text_holo_light.xml b/flock/src/main/res/drawable/flocktheme_edit_text_holo_light.xml new file mode 100755 index 0000000..bc48c2f --- /dev/null +++ b/flock/src/main/res/drawable/flocktheme_edit_text_holo_light.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/flock/src/main/res/drawable/flocktheme_fastscroll_thumb_holo.xml b/flock/src/main/res/drawable/flocktheme_fastscroll_thumb_holo.xml new file mode 100755 index 0000000..06d8731 --- /dev/null +++ b/flock/src/main/res/drawable/flocktheme_fastscroll_thumb_holo.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/flock/src/main/res/drawable/flocktheme_item_background_holo_light.xml b/flock/src/main/res/drawable/flocktheme_item_background_holo_light.xml new file mode 100755 index 0000000..cb341e7 --- /dev/null +++ b/flock/src/main/res/drawable/flocktheme_item_background_holo_light.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/flock/src/main/res/drawable/flocktheme_list_selector_background_transition_holo_light.xml b/flock/src/main/res/drawable/flocktheme_list_selector_background_transition_holo_light.xml new file mode 100755 index 0000000..16aa4c1 --- /dev/null +++ b/flock/src/main/res/drawable/flocktheme_list_selector_background_transition_holo_light.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/flock/src/main/res/drawable/flocktheme_list_selector_holo_light.xml b/flock/src/main/res/drawable/flocktheme_list_selector_holo_light.xml new file mode 100755 index 0000000..b9ffc96 --- /dev/null +++ b/flock/src/main/res/drawable/flocktheme_list_selector_holo_light.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + diff --git a/flock/src/main/res/drawable/flocktheme_progress_horizontal_holo_light.xml b/flock/src/main/res/drawable/flocktheme_progress_horizontal_holo_light.xml new file mode 100755 index 0000000..3e42c42 --- /dev/null +++ b/flock/src/main/res/drawable/flocktheme_progress_horizontal_holo_light.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + diff --git a/flock/src/main/res/drawable/flocktheme_progress_horizontal_holo_light_green.xml b/flock/src/main/res/drawable/flocktheme_progress_horizontal_holo_light_green.xml new file mode 100644 index 0000000..a794bb0 --- /dev/null +++ b/flock/src/main/res/drawable/flocktheme_progress_horizontal_holo_light_green.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/flock/src/main/res/drawable/flocktheme_progress_horizontal_holo_light_red.xml b/flock/src/main/res/drawable/flocktheme_progress_horizontal_holo_light_red.xml new file mode 100644 index 0000000..2b1a5f8 --- /dev/null +++ b/flock/src/main/res/drawable/flocktheme_progress_horizontal_holo_light_red.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/flock/src/main/res/drawable/flocktheme_progress_horizontal_holo_light_yellow.xml b/flock/src/main/res/drawable/flocktheme_progress_horizontal_holo_light_yellow.xml new file mode 100644 index 0000000..66849d6 --- /dev/null +++ b/flock/src/main/res/drawable/flocktheme_progress_horizontal_holo_light_yellow.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/flock/src/main/res/drawable/flocktheme_progress_indeterminate_horizontal_holo_light.xml b/flock/src/main/res/drawable/flocktheme_progress_indeterminate_horizontal_holo_light.xml new file mode 100755 index 0000000..e78b36f --- /dev/null +++ b/flock/src/main/res/drawable/flocktheme_progress_indeterminate_horizontal_holo_light.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + diff --git a/flock/src/main/res/drawable/flocktheme_scrubber_control_selector_holo_light.xml b/flock/src/main/res/drawable/flocktheme_scrubber_control_selector_holo_light.xml new file mode 100755 index 0000000..8253115 --- /dev/null +++ b/flock/src/main/res/drawable/flocktheme_scrubber_control_selector_holo_light.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/flock/src/main/res/drawable/flocktheme_scrubber_progress_horizontal_holo_light.xml b/flock/src/main/res/drawable/flocktheme_scrubber_progress_horizontal_holo_light.xml new file mode 100755 index 0000000..0140751 --- /dev/null +++ b/flock/src/main/res/drawable/flocktheme_scrubber_progress_horizontal_holo_light.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/flock/src/main/res/drawable/flocktheme_spinner_background_holo_light.xml b/flock/src/main/res/drawable/flocktheme_spinner_background_holo_light.xml new file mode 100755 index 0000000..03970e3 --- /dev/null +++ b/flock/src/main/res/drawable/flocktheme_spinner_background_holo_light.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/flock/src/main/res/drawable/flocktheme_switch_inner_holo_light.xml b/flock/src/main/res/drawable/flocktheme_switch_inner_holo_light.xml new file mode 100755 index 0000000..544ae6f --- /dev/null +++ b/flock/src/main/res/drawable/flocktheme_switch_inner_holo_light.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/flock/src/main/res/drawable/flocktheme_switch_track_holo_light.xml b/flock/src/main/res/drawable/flocktheme_switch_track_holo_light.xml new file mode 100755 index 0000000..40aa96c --- /dev/null +++ b/flock/src/main/res/drawable/flocktheme_switch_track_holo_light.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/flock/src/main/res/drawable/rounded_thing_grey.xml b/flock/src/main/res/drawable/rounded_thing_grey.xml new file mode 100644 index 0000000..831b818 --- /dev/null +++ b/flock/src/main/res/drawable/rounded_thing_grey.xml @@ -0,0 +1,32 @@ + + + + + + + + + \ No newline at end of file diff --git a/flock/src/main/res/drawable/rounded_thing_orange.xml b/flock/src/main/res/drawable/rounded_thing_orange.xml new file mode 100644 index 0000000..d17e8d6 --- /dev/null +++ b/flock/src/main/res/drawable/rounded_thing_orange.xml @@ -0,0 +1,32 @@ + + + + + + + + + \ No newline at end of file diff --git a/flock/src/main/res/drawable/selectable_item_background.xml b/flock/src/main/res/drawable/selectable_item_background.xml new file mode 100644 index 0000000..8639e7d --- /dev/null +++ b/flock/src/main/res/drawable/selectable_item_background.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/flock/src/main/res/layout-land/activity_manage_subscription.xml b/flock/src/main/res/layout-land/activity_manage_subscription.xml new file mode 100644 index 0000000..9ea2308 --- /dev/null +++ b/flock/src/main/res/layout-land/activity_manage_subscription.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +