From c8a31c0ef210749f80d31546b77de2becd5b7e1c Mon Sep 17 00:00:00 2001 From: Avram Lyon Date: Fri, 1 Apr 2016 23:32:39 -0700 Subject: [PATCH] Zotable changeset. From code hosted at https://drive.google.com/file/d/0B2k1aKFvEwTxRHZPU3FfT0hLWU0/view App at https://play.google.com/store/apps/details?id=com.mattrobertson.zotable.app --- build.gradle | 29 +- .../com/gimranov/zandy/app/test/ApiTest.java | 81 -- .../com/gimranov/zandy/app/test/MainTest.java | 27 - src/main/AndroidManifest.xml | 75 +- .../com/gimranov/zandy/app/Application.java | 28 - .../zandy/app/CollectionActivity.java | 310 ------ .../app/CollectionMembershipActivity.java | 249 ----- .../gimranov/zandy/app/CreatorActivity.java | 381 -------- .../gimranov/zandy/app/LookupActivity.java | 260 ----- .../com/gimranov/zandy/app/MainActivity.java | 479 --------- .../com/gimranov/zandy/app/Persistence.java | 26 - .../com/gimranov/zandy/app/SyncEvent.java | 19 - .../com/gimranov/zandy/app/TagActivity.java | 264 ----- .../java/com/gimranov/zandy/app/Util.java | 18 - .../com/gimranov/zandy/app/task/APIEvent.java | 10 - .../zotable/app/Activity_Main.java | 500 ++++++++++ .../zotable/app/Activity_Preference.java | 96 ++ .../zotable}/app/AmazonZxingGlue.java | 18 +- .../zotable/app/Application.java | 44 + .../zotable}/app/AttachmentActivity.java | 51 +- .../zotable/app/CollectionActivity.java | 223 +++++ .../app/CollectionMembershipActivity.java | 304 ++++++ .../zotable/app/CreatorActivity.java | 369 +++++++ .../zotable/app/Fragment_Collections.java | 707 ++++++++++++++ .../zotable/app/Fragment_Favorites.java | 585 +++++++++++ .../zotable/app/Fragment_Items.java | 915 ++++++++++++++++++ .../zotable/app/Fragment_Tags.java | 645 ++++++++++++ .../zotable}/app/ItemActivity.java | 320 +++--- .../zotable}/app/ItemDataActivity.java | 230 +++-- .../zotable}/app/NoteActivity.java | 23 +- .../zotable/app/PDFActivity.java | 75 ++ .../zotable/app/Persistence.java | 42 + .../zotable}/app/Query.java | 22 +- .../zotable}/app/RequestActivity.java | 17 +- .../zotable/app/SearchActivity.java | 139 +++ .../zotable}/app/ServerCredentials.java | 22 +- .../zotable}/app/SettingsActivity.java | 19 +- .../mattrobertson/zotable/app/SyncEvent.java | 35 + .../zotable/app/TagActivity.java | 262 +++++ .../zotable/app/TagItemsActivity.java | 610 ++++++++++++ .../com/mattrobertson/zotable/app/Util.java | 105 ++ .../zotable}/app/XMLResponseParser.java | 34 +- .../zotable}/app/data/Attachment.java | 28 +- .../zotable/app/data/CollectionAdapter.java | 227 +++++ .../zotable}/app/data/Creator.java | 12 +- .../zotable}/app/data/Database.java | 57 +- .../zotable}/app/data/Item.java | 208 +++- .../zotable}/app/data/ItemAdapter.java | 15 +- .../zotable}/app/data/ItemCollection.java | 110 ++- .../zotable/app/data/TagAdapter.java} | 50 +- .../zotable/app/task/APIEvent.java | 26 + .../zotable}/app/task/APIException.java | 10 +- .../zotable}/app/task/APIRequest.java | 32 +- .../zotable}/app/task/ZoteroAPITask.java | 63 +- .../zotable}/app/webdav/WebDavTrust.java | 18 +- src/main/java/org/pdfparse/PDFDefines.java | 28 + .../java/org/pdfparse/cds/PDFRectangle.java | 156 +++ src/main/java/org/pdfparse/cos/COSArray.java | 92 ++ src/main/java/org/pdfparse/cos/COSBool.java | 56 ++ .../java/org/pdfparse/cos/COSDictionary.java | 320 ++++++ src/main/java/org/pdfparse/cos/COSName.java | 274 ++++++ src/main/java/org/pdfparse/cos/COSNull.java | 48 + src/main/java/org/pdfparse/cos/COSNumber.java | 279 ++++++ src/main/java/org/pdfparse/cos/COSObject.java | 34 + .../java/org/pdfparse/cos/COSReference.java | 62 ++ src/main/java/org/pdfparse/cos/COSStream.java | 55 ++ src/main/java/org/pdfparse/cos/COSString.java | 585 +++++++++++ .../pdfparse/exception/EDateConvertError.java | 48 + .../pdfparse/exception/EDecoderException.java | 45 + .../org/pdfparse/exception/ENotSupported.java | 44 + .../org/pdfparse/exception/EParseError.java | 56 ++ .../java/org/pdfparse/filter/LZWDecoder.java | 242 +++++ .../org/pdfparse/filter/StreamDecoder.java | 480 +++++++++ .../org/pdfparse/filter/TIFFLZWDecoder.java | 273 ++++++ .../org/pdfparse/model/PDFDocCatalog.java | 190 ++++ .../java/org/pdfparse/model/PDFDocInfo.java | 304 ++++++ .../java/org/pdfparse/model/PDFDocument.java | 210 ++++ src/main/java/org/pdfparse/model/PDFPage.java | 138 +++ .../java/org/pdfparse/model/PDFPageNode.java | 98 ++ .../java/org/pdfparse/parser/PDFParser.java | 871 +++++++++++++++++ .../java/org/pdfparse/parser/PDFRawData.java | 259 +++++ .../org/pdfparse/parser/ParsingContext.java | 71 ++ .../org/pdfparse/parser/ParsingEvent.java | 33 + .../org/pdfparse/parser/ParsingGetObject.java | 29 + .../java/org/pdfparse/parser/XRefEntry.java | 54 ++ .../java/org/pdfparse/utils/ByteBuffer.java | 228 +++++ .../org/pdfparse/utils/DateConverter.java | 363 +++++++ .../org/pdfparse/utils/IntIntHashtable.java | 480 +++++++++ .../org/pdfparse/utils/IntObjHashtable.java | 480 +++++++++ .../drawable-hdpi/ic_action_content_new.png | Bin 0 -> 322 bytes src/main/res/drawable-hdpi/ic_browse.png | Bin 0 -> 1139 bytes .../res/drawable-hdpi/ic_collection_black.png | Bin 0 -> 554 bytes .../res/drawable-hdpi/ic_collection_blue.png | Bin 0 -> 613 bytes src/main/res/drawable-hdpi/ic_delete.png | Bin 0 -> 451 bytes src/main/res/drawable-hdpi/ic_down.png | Bin 0 -> 467 bytes src/main/res/drawable-hdpi/ic_edit.png | Bin 0 -> 556 bytes .../res/drawable-hdpi/ic_favorites_black.png | Bin 0 -> 800 bytes .../res/drawable-hdpi/ic_favorites_blue.png | Bin 0 -> 1054 bytes src/main/res/drawable-hdpi/ic_isbn.png | Bin 0 -> 1032 bytes src/main/res/drawable-hdpi/ic_item_black.png | Bin 0 -> 483 bytes src/main/res/drawable-hdpi/ic_item_blue.png | Bin 0 -> 538 bytes src/main/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 2347 bytes src/main/res/drawable-hdpi/ic_manual.png | Bin 0 -> 308 bytes src/main/res/drawable-hdpi/ic_plus.png | Bin 0 -> 412 bytes src/main/res/drawable-hdpi/ic_scan.png | Bin 0 -> 427 bytes src/main/res/drawable-hdpi/ic_settings.png | Bin 0 -> 762 bytes src/main/res/drawable-hdpi/ic_star_empty.png | Bin 0 -> 1441 bytes src/main/res/drawable-hdpi/ic_star_filled.png | Bin 0 -> 1057 bytes src/main/res/drawable-hdpi/ic_tag_black.png | Bin 0 -> 508 bytes src/main/res/drawable-hdpi/ic_tag_blue.png | Bin 0 -> 607 bytes src/main/res/drawable-hdpi/ic_upload.png | Bin 0 -> 356 bytes src/main/res/drawable-hdpi/icon.png | Bin 4147 -> 0 bytes src/main/res/drawable-ldpi/icon.png | Bin 1723 -> 0 bytes src/main/res/drawable-mdpi/ic_browse.png | Bin 0 -> 674 bytes .../res/drawable-mdpi/ic_collection_black.png | Bin 0 -> 275 bytes .../res/drawable-mdpi/ic_collection_blue.png | Bin 0 -> 290 bytes src/main/res/drawable-mdpi/ic_delete.png | Bin 0 -> 271 bytes src/main/res/drawable-mdpi/ic_down.png | Bin 0 -> 354 bytes src/main/res/drawable-mdpi/ic_edit.png | Bin 0 -> 392 bytes .../res/drawable-mdpi/ic_favorites_black.png | Bin 0 -> 512 bytes .../res/drawable-mdpi/ic_favorites_blue.png | Bin 0 -> 681 bytes src/main/res/drawable-mdpi/ic_item_black.png | Bin 0 -> 304 bytes src/main/res/drawable-mdpi/ic_item_blue.png | Bin 0 -> 330 bytes src/main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 1604 bytes src/main/res/drawable-mdpi/ic_plus.png | Bin 0 -> 295 bytes src/main/res/drawable-mdpi/ic_scan.png | Bin 0 -> 448 bytes src/main/res/drawable-mdpi/ic_settings.png | Bin 0 -> 442 bytes src/main/res/drawable-mdpi/ic_star_empty.png | Bin 0 -> 875 bytes src/main/res/drawable-mdpi/ic_star_filled.png | Bin 0 -> 680 bytes src/main/res/drawable-mdpi/ic_tag_black.png | Bin 0 -> 307 bytes src/main/res/drawable-mdpi/ic_tag_blue.png | Bin 0 -> 367 bytes src/main/res/drawable-mdpi/ic_upload.png | Bin 0 -> 232 bytes src/main/res/drawable-mdpi/icon.png | Bin 2574 -> 0 bytes .../drawable-xhdpi/ic_action_content_new.png | Bin 0 -> 363 bytes src/main/res/drawable-xhdpi/ic_browse.png | Bin 0 -> 1520 bytes .../drawable-xhdpi/ic_collection_black.png | Bin 0 -> 559 bytes .../res/drawable-xhdpi/ic_collection_blue.png | Bin 0 -> 596 bytes src/main/res/drawable-xhdpi/ic_delete.png | Bin 0 -> 491 bytes src/main/res/drawable-xhdpi/ic_down.png | Bin 0 -> 618 bytes src/main/res/drawable-xhdpi/ic_edit.png | Bin 0 -> 687 bytes .../res/drawable-xhdpi/ic_favorites_black.png | Bin 0 -> 1132 bytes .../res/drawable-xhdpi/ic_favorites_blue.png | Bin 0 -> 1496 bytes src/main/res/drawable-xhdpi/ic_isbn.png | Bin 0 -> 1447 bytes src/main/res/drawable-xhdpi/ic_item_black.png | Bin 0 -> 613 bytes src/main/res/drawable-xhdpi/ic_item_blue.png | Bin 0 -> 656 bytes src/main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 3126 bytes src/main/res/drawable-xhdpi/ic_manual.png | Bin 0 -> 365 bytes src/main/res/drawable-xhdpi/ic_plus.png | Bin 0 -> 482 bytes src/main/res/drawable-xhdpi/ic_settings.png | Bin 0 -> 1032 bytes src/main/res/drawable-xhdpi/ic_star_empty.png | Bin 0 -> 1956 bytes .../res/drawable-xhdpi/ic_star_filled.png | Bin 0 -> 1509 bytes src/main/res/drawable-xhdpi/ic_tag_black.png | Bin 0 -> 701 bytes src/main/res/drawable-xhdpi/ic_tag_blue.png | Bin 0 -> 844 bytes src/main/res/drawable-xhdpi/ic_upload.png | Bin 0 -> 434 bytes .../drawable-xxhdpi/ic_action_content_new.png | Bin 0 -> 603 bytes src/main/res/drawable-xxhdpi/ic_browse.png | Bin 0 -> 2460 bytes .../drawable-xxhdpi/ic_collection_black.png | Bin 0 -> 980 bytes .../drawable-xxhdpi/ic_collection_blue.png | Bin 0 -> 981 bytes src/main/res/drawable-xxhdpi/ic_delete.png | Bin 0 -> 851 bytes src/main/res/drawable-xxhdpi/ic_down.png | Bin 0 -> 833 bytes src/main/res/drawable-xxhdpi/ic_edit.png | Bin 0 -> 1047 bytes .../drawable-xxhdpi/ic_favorites_black.png | Bin 0 -> 1730 bytes .../res/drawable-xxhdpi/ic_favorites_blue.png | Bin 0 -> 2219 bytes src/main/res/drawable-xxhdpi/ic_isbn.png | Bin 0 -> 2151 bytes .../res/drawable-xxhdpi/ic_item_black.png | Bin 0 -> 1078 bytes src/main/res/drawable-xxhdpi/ic_item_blue.png | Bin 0 -> 1138 bytes src/main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 4911 bytes src/main/res/drawable-xxhdpi/ic_manual.png | Bin 0 -> 710 bytes src/main/res/drawable-xxhdpi/ic_plus.png | Bin 0 -> 874 bytes src/main/res/drawable-xxhdpi/ic_scan.png | Bin 0 -> 764 bytes src/main/res/drawable-xxhdpi/ic_settings.png | Bin 0 -> 1706 bytes .../res/drawable-xxhdpi/ic_star_empty.png | Bin 0 -> 3013 bytes .../res/drawable-xxhdpi/ic_star_filled.png | Bin 0 -> 2249 bytes src/main/res/drawable-xxhdpi/ic_tag_black.png | Bin 0 -> 1121 bytes src/main/res/drawable-xxhdpi/ic_tag_blue.png | Bin 0 -> 1308 bytes src/main/res/drawable-xxhdpi/ic_upload.png | Bin 0 -> 770 bytes .../ic_action_content_new.png | Bin 0 -> 1077 bytes src/main/res/drawable-xxxhdpi/ic_browse.png | Bin 0 -> 3853 bytes .../drawable-xxxhdpi/ic_collection_black.png | Bin 0 -> 1508 bytes .../drawable-xxxhdpi/ic_collection_blue.png | Bin 0 -> 1673 bytes src/main/res/drawable-xxxhdpi/ic_delete.png | Bin 0 -> 1446 bytes src/main/res/drawable-xxxhdpi/ic_down.png | Bin 0 -> 1505 bytes src/main/res/drawable-xxxhdpi/ic_edit.png | Bin 0 -> 1696 bytes .../drawable-xxxhdpi/ic_favorites_black.png | Bin 0 -> 2490 bytes .../drawable-xxxhdpi/ic_favorites_blue.png | Bin 0 -> 3206 bytes src/main/res/drawable-xxxhdpi/ic_isbn.png | Bin 0 -> 3096 bytes .../res/drawable-xxxhdpi/ic_item_black.png | Bin 0 -> 1645 bytes .../res/drawable-xxxhdpi/ic_item_blue.png | Bin 0 -> 1640 bytes src/main/res/drawable-xxxhdpi/ic_launcher.png | Bin 0 -> 7121 bytes src/main/res/drawable-xxxhdpi/ic_manual.png | Bin 0 -> 1003 bytes src/main/res/drawable-xxxhdpi/ic_plus.png | Bin 0 -> 1156 bytes src/main/res/drawable-xxxhdpi/ic_scan.png | Bin 0 -> 1136 bytes src/main/res/drawable-xxxhdpi/ic_settings.png | Bin 0 -> 2644 bytes .../res/drawable-xxxhdpi/ic_star_empty.png | Bin 0 -> 4279 bytes .../res/drawable-xxxhdpi/ic_star_filled.png | Bin 0 -> 3212 bytes .../res/drawable-xxxhdpi/ic_tag_black.png | Bin 0 -> 1848 bytes src/main/res/drawable-xxxhdpi/ic_tag_blue.png | Bin 0 -> 2146 bytes src/main/res/drawable-xxxhdpi/ic_upload.png | Bin 0 -> 1239 bytes src/main/res/drawable/bg_light.xml | 4 + src/main/res/drawable/bg_primary_light.xml | 4 + src/main/res/drawable/book.png | Bin src/main/res/drawable/book_open.png | Bin src/main/res/drawable/comment.png | Bin src/main/res/drawable/email.png | Bin .../res/drawable/fab_label_background.xml | 11 + src/main/res/drawable/film.png | Bin src/main/res/drawable/folder.png | Bin src/main/res/drawable/glyphish_02_redo.png | Bin 3315 -> 0 bytes .../res/drawable/glyphish_104_index_cards.png | Bin 2895 -> 0 bytes .../res/drawable/glyphish_106_sliders.png | Bin 2902 -> 0 bytes src/main/res/drawable/glyphish_10_medical.png | Bin 2915 -> 0 bytes .../res/drawable/glyphish_151_telescope.png | Bin 370 -> 0 bytes .../res/drawable/glyphish_195_barcode.png | Bin 147 -> 0 bytes .../drawable/glyphish_22_skull_n_bones.png | Bin 3595 -> 0 bytes src/main/res/drawable/glyphish_59_flag.png | Bin 2954 -> 0 bytes .../res/drawable/ic_action_content_new.png | Bin 0 -> 322 bytes src/main/res/drawable/layout.png | Bin .../res/drawable/list_child_indicator.xml | 0 src/main/res/drawable/map.png | Bin src/main/res/drawable/newspaper.png | Bin src/main/res/drawable/note.png | Bin src/main/res/drawable/page.png | Bin src/main/res/drawable/page_white.png | Bin src/main/res/drawable/page_white_acrobat.png | Bin .../res/drawable/page_white_powerpoint.png | Bin src/main/res/drawable/page_white_text.png | Bin .../res/drawable/page_white_text_width.png | Bin src/main/res/drawable/page_white_width.png | Bin src/main/res/drawable/picture.png | Bin src/main/res/drawable/report.png | Bin src/main/res/drawable/report_user.png | Bin src/main/res/drawable/script.png | Bin src/main/res/drawable/state_list_item.xml | 10 + src/main/res/drawable/television.png | Bin src/main/res/drawable/zandy72.png | Bin 4448 -> 0 bytes src/main/res/layout-sw600dp/collections.xml | 19 + src/main/res/layout/activity_main.xml | 214 ++++ src/main/res/layout/activity_pdf_handler.xml | 17 + src/main/res/layout/coll_mem_activity.xml | 33 + src/main/res/layout/collections.xml | 33 +- src/main/res/layout/collections_old.xml | 19 + src/main/res/layout/creator_activity.xml | 33 + src/main/res/layout/creator_dialog.xml | 107 +- src/main/res/layout/fragment_collections.xml | 86 ++ src/main/res/layout/fragment_favorites.xml | 92 ++ src/main/res/layout/fragment_items.xml | 92 ++ src/main/res/layout/fragment_tags.xml | 89 ++ src/main/res/layout/items.xml | 122 ++- src/main/res/layout/json.xml | 17 +- src/main/res/layout/list_attach.xml | 20 +- src/main/res/layout/list_coll_mem.xml | 28 + src/main/res/layout/list_collection.xml | 83 +- src/main/res/layout/list_data.xml | 78 +- src/main/res/layout/list_item.xml | 25 +- src/main/res/layout/list_tag.xml | 29 + src/main/res/layout/lookup.xml | 17 +- src/main/res/layout/main.xml | 17 +- src/main/res/layout/note.xml | 106 +- src/main/res/layout/preference_layout.xml | 12 + src/main/res/layout/preferences.xml | 42 - src/main/res/layout/preferences_old.xml | 38 + src/main/res/layout/requests.xml | 17 +- src/main/res/layout/search_activity.xml | 93 ++ src/main/res/layout/tag_activity.xml | 33 + src/main/res/layout/tag_items.xml | 76 ++ .../res/menu/menu_activity__preference.xml | 7 + src/main/res/menu/note_menu.xml | 17 +- src/main/res/menu/zotero_menu.xml | 76 +- src/main/res/menu/zzz.xml | 7 + src/main/res/values-en/strings.xml | 21 +- src/main/res/values-es/strings.xml | 10 +- .../drawable-hdpi/ic_favorites_blue.png | Bin 0 -> 1054 bytes .../values-fr/drawable-hdpi/ic_tag_blue.png | Bin 0 -> 607 bytes .../drawable-mdpi/ic_favorites_blue.png | Bin 0 -> 681 bytes .../values-fr/drawable-mdpi/ic_tag_blue.png | Bin 0 -> 367 bytes .../drawable-xhdpi/ic_favorites_blue.png | Bin 0 -> 1496 bytes .../values-fr/drawable-xhdpi/ic_tag_blue.png | Bin 0 -> 844 bytes .../drawable-xxhdpi/ic_favorites_blue.png | Bin 0 -> 2219 bytes .../values-fr/drawable-xxhdpi/ic_tag_blue.png | Bin 0 -> 1308 bytes .../drawable-xxxhdpi/ic_favorites_blue.png | Bin 0 -> 3206 bytes .../drawable-xxxhdpi/ic_tag_blue.png | Bin 0 -> 2146 bytes src/main/res/values-fr/strings.xml | 25 +- src/main/res/values-pt/strings.xml | 10 +- src/main/res/values-ru/strings.xml | 19 +- src/main/res/values-w820dp/dimens.xml | 6 + src/main/res/values/colors.xml | 35 + src/main/res/values/dimens.xml | 5 + src/main/res/values/strings.xml | 53 +- src/main/res/values/styles.xml | 49 + .../xml/drawable-hdpi/ic_favorites_black.png | Bin 0 -> 800 bytes .../xml/drawable-mdpi/ic_favorites_black.png | Bin 0 -> 512 bytes .../xml/drawable-xhdpi/ic_favorites_black.png | Bin 0 -> 1132 bytes .../drawable-xxhdpi/ic_favorites_black.png | Bin 0 -> 1730 bytes .../drawable-xxxhdpi/ic_favorites_black.png | Bin 0 -> 2490 bytes src/main/res/xml/searchable.xml | 0 src/main/res/xml/settings.xml | 33 +- 296 files changed, 15372 insertions(+), 3226 deletions(-) delete mode 100644 src/instrumentTest/java/com/gimranov/zandy/app/test/ApiTest.java delete mode 100644 src/instrumentTest/java/com/gimranov/zandy/app/test/MainTest.java mode change 100644 => 100755 src/main/AndroidManifest.xml delete mode 100644 src/main/java/com/gimranov/zandy/app/Application.java delete mode 100644 src/main/java/com/gimranov/zandy/app/CollectionActivity.java delete mode 100644 src/main/java/com/gimranov/zandy/app/CollectionMembershipActivity.java delete mode 100644 src/main/java/com/gimranov/zandy/app/CreatorActivity.java delete mode 100644 src/main/java/com/gimranov/zandy/app/LookupActivity.java delete mode 100644 src/main/java/com/gimranov/zandy/app/MainActivity.java delete mode 100644 src/main/java/com/gimranov/zandy/app/Persistence.java delete mode 100644 src/main/java/com/gimranov/zandy/app/SyncEvent.java delete mode 100644 src/main/java/com/gimranov/zandy/app/TagActivity.java delete mode 100644 src/main/java/com/gimranov/zandy/app/Util.java delete mode 100644 src/main/java/com/gimranov/zandy/app/task/APIEvent.java create mode 100755 src/main/java/com/mattrobertson/zotable/app/Activity_Main.java create mode 100755 src/main/java/com/mattrobertson/zotable/app/Activity_Preference.java rename src/main/java/com/{gimranov/zandy => mattrobertson/zotable}/app/AmazonZxingGlue.java (64%) mode change 100644 => 100755 create mode 100755 src/main/java/com/mattrobertson/zotable/app/Application.java rename src/main/java/com/{gimranov/zandy => mattrobertson/zotable}/app/AttachmentActivity.java (93%) mode change 100644 => 100755 create mode 100755 src/main/java/com/mattrobertson/zotable/app/CollectionActivity.java create mode 100755 src/main/java/com/mattrobertson/zotable/app/CollectionMembershipActivity.java create mode 100755 src/main/java/com/mattrobertson/zotable/app/CreatorActivity.java create mode 100755 src/main/java/com/mattrobertson/zotable/app/Fragment_Collections.java create mode 100755 src/main/java/com/mattrobertson/zotable/app/Fragment_Favorites.java create mode 100755 src/main/java/com/mattrobertson/zotable/app/Fragment_Items.java create mode 100755 src/main/java/com/mattrobertson/zotable/app/Fragment_Tags.java rename src/main/java/com/{gimranov/zandy => mattrobertson/zotable}/app/ItemActivity.java (74%) mode change 100644 => 100755 rename src/main/java/com/{gimranov/zandy => mattrobertson/zotable}/app/ItemDataActivity.java (74%) mode change 100644 => 100755 rename src/main/java/com/{gimranov/zandy => mattrobertson/zotable}/app/NoteActivity.java (86%) mode change 100644 => 100755 create mode 100755 src/main/java/com/mattrobertson/zotable/app/PDFActivity.java create mode 100755 src/main/java/com/mattrobertson/zotable/app/Persistence.java rename src/main/java/com/{gimranov/zandy => mattrobertson/zotable}/app/Query.java (63%) mode change 100644 => 100755 rename src/main/java/com/{gimranov/zandy => mattrobertson/zotable}/app/RequestActivity.java (87%) mode change 100644 => 100755 create mode 100755 src/main/java/com/mattrobertson/zotable/app/SearchActivity.java rename src/main/java/com/{gimranov/zandy => mattrobertson/zotable}/app/ServerCredentials.java (87%) mode change 100644 => 100755 rename src/main/java/com/{gimranov/zandy => mattrobertson/zotable}/app/SettingsActivity.java (86%) mode change 100644 => 100755 create mode 100755 src/main/java/com/mattrobertson/zotable/app/SyncEvent.java create mode 100755 src/main/java/com/mattrobertson/zotable/app/TagActivity.java create mode 100755 src/main/java/com/mattrobertson/zotable/app/TagItemsActivity.java create mode 100755 src/main/java/com/mattrobertson/zotable/app/Util.java rename src/main/java/com/{gimranov/zandy => mattrobertson/zotable}/app/XMLResponseParser.java (94%) mode change 100644 => 100755 rename src/main/java/com/{gimranov/zandy => mattrobertson/zotable}/app/data/Attachment.java (85%) mode change 100644 => 100755 create mode 100755 src/main/java/com/mattrobertson/zotable/app/data/CollectionAdapter.java rename src/main/java/com/{gimranov/zandy => mattrobertson/zotable}/app/data/Creator.java (89%) mode change 100644 => 100755 rename src/main/java/com/{gimranov/zandy => mattrobertson/zotable}/app/data/Database.java (83%) mode change 100644 => 100755 rename src/main/java/com/{gimranov/zandy => mattrobertson/zotable}/app/data/Item.java (90%) mode change 100644 => 100755 rename src/main/java/com/{gimranov/zandy => mattrobertson/zotable}/app/data/ItemAdapter.java (86%) mode change 100644 => 100755 rename src/main/java/com/{gimranov/zandy => mattrobertson/zotable}/app/data/ItemCollection.java (88%) mode change 100644 => 100755 rename src/main/java/com/{gimranov/zandy/app/data/CollectionAdapter.java => mattrobertson/zotable/app/data/TagAdapter.java} (55%) mode change 100644 => 100755 create mode 100755 src/main/java/com/mattrobertson/zotable/app/task/APIEvent.java rename src/main/java/com/{gimranov/zandy => mattrobertson/zotable}/app/task/APIException.java (81%) mode change 100644 => 100755 rename src/main/java/com/{gimranov/zandy => mattrobertson/zotable}/app/task/APIRequest.java (97%) mode change 100644 => 100755 rename src/main/java/com/{gimranov/zandy => mattrobertson/zotable}/app/task/ZoteroAPITask.java (74%) mode change 100644 => 100755 rename src/main/java/com/{gimranov/zandy => mattrobertson/zotable}/app/webdav/WebDavTrust.java (60%) mode change 100644 => 100755 create mode 100755 src/main/java/org/pdfparse/PDFDefines.java create mode 100755 src/main/java/org/pdfparse/cds/PDFRectangle.java create mode 100755 src/main/java/org/pdfparse/cos/COSArray.java create mode 100755 src/main/java/org/pdfparse/cos/COSBool.java create mode 100755 src/main/java/org/pdfparse/cos/COSDictionary.java create mode 100755 src/main/java/org/pdfparse/cos/COSName.java create mode 100755 src/main/java/org/pdfparse/cos/COSNull.java create mode 100755 src/main/java/org/pdfparse/cos/COSNumber.java create mode 100755 src/main/java/org/pdfparse/cos/COSObject.java create mode 100755 src/main/java/org/pdfparse/cos/COSReference.java create mode 100755 src/main/java/org/pdfparse/cos/COSStream.java create mode 100755 src/main/java/org/pdfparse/cos/COSString.java create mode 100755 src/main/java/org/pdfparse/exception/EDateConvertError.java create mode 100755 src/main/java/org/pdfparse/exception/EDecoderException.java create mode 100755 src/main/java/org/pdfparse/exception/ENotSupported.java create mode 100755 src/main/java/org/pdfparse/exception/EParseError.java create mode 100755 src/main/java/org/pdfparse/filter/LZWDecoder.java create mode 100755 src/main/java/org/pdfparse/filter/StreamDecoder.java create mode 100755 src/main/java/org/pdfparse/filter/TIFFLZWDecoder.java create mode 100755 src/main/java/org/pdfparse/model/PDFDocCatalog.java create mode 100755 src/main/java/org/pdfparse/model/PDFDocInfo.java create mode 100755 src/main/java/org/pdfparse/model/PDFDocument.java create mode 100755 src/main/java/org/pdfparse/model/PDFPage.java create mode 100755 src/main/java/org/pdfparse/model/PDFPageNode.java create mode 100755 src/main/java/org/pdfparse/parser/PDFParser.java create mode 100755 src/main/java/org/pdfparse/parser/PDFRawData.java create mode 100755 src/main/java/org/pdfparse/parser/ParsingContext.java create mode 100755 src/main/java/org/pdfparse/parser/ParsingEvent.java create mode 100755 src/main/java/org/pdfparse/parser/ParsingGetObject.java create mode 100755 src/main/java/org/pdfparse/parser/XRefEntry.java create mode 100755 src/main/java/org/pdfparse/utils/ByteBuffer.java create mode 100755 src/main/java/org/pdfparse/utils/DateConverter.java create mode 100755 src/main/java/org/pdfparse/utils/IntIntHashtable.java create mode 100755 src/main/java/org/pdfparse/utils/IntObjHashtable.java create mode 100755 src/main/res/drawable-hdpi/ic_action_content_new.png create mode 100755 src/main/res/drawable-hdpi/ic_browse.png create mode 100755 src/main/res/drawable-hdpi/ic_collection_black.png create mode 100755 src/main/res/drawable-hdpi/ic_collection_blue.png create mode 100755 src/main/res/drawable-hdpi/ic_delete.png create mode 100755 src/main/res/drawable-hdpi/ic_down.png create mode 100755 src/main/res/drawable-hdpi/ic_edit.png create mode 100755 src/main/res/drawable-hdpi/ic_favorites_black.png create mode 100755 src/main/res/drawable-hdpi/ic_favorites_blue.png create mode 100755 src/main/res/drawable-hdpi/ic_isbn.png create mode 100755 src/main/res/drawable-hdpi/ic_item_black.png create mode 100755 src/main/res/drawable-hdpi/ic_item_blue.png create mode 100755 src/main/res/drawable-hdpi/ic_launcher.png create mode 100755 src/main/res/drawable-hdpi/ic_manual.png create mode 100755 src/main/res/drawable-hdpi/ic_plus.png create mode 100755 src/main/res/drawable-hdpi/ic_scan.png create mode 100755 src/main/res/drawable-hdpi/ic_settings.png create mode 100755 src/main/res/drawable-hdpi/ic_star_empty.png create mode 100755 src/main/res/drawable-hdpi/ic_star_filled.png create mode 100755 src/main/res/drawable-hdpi/ic_tag_black.png create mode 100755 src/main/res/drawable-hdpi/ic_tag_blue.png create mode 100755 src/main/res/drawable-hdpi/ic_upload.png delete mode 100644 src/main/res/drawable-hdpi/icon.png delete mode 100644 src/main/res/drawable-ldpi/icon.png create mode 100755 src/main/res/drawable-mdpi/ic_browse.png create mode 100755 src/main/res/drawable-mdpi/ic_collection_black.png create mode 100755 src/main/res/drawable-mdpi/ic_collection_blue.png create mode 100755 src/main/res/drawable-mdpi/ic_delete.png create mode 100755 src/main/res/drawable-mdpi/ic_down.png create mode 100755 src/main/res/drawable-mdpi/ic_edit.png create mode 100755 src/main/res/drawable-mdpi/ic_favorites_black.png create mode 100755 src/main/res/drawable-mdpi/ic_favorites_blue.png create mode 100755 src/main/res/drawable-mdpi/ic_item_black.png create mode 100755 src/main/res/drawable-mdpi/ic_item_blue.png create mode 100755 src/main/res/drawable-mdpi/ic_launcher.png create mode 100755 src/main/res/drawable-mdpi/ic_plus.png create mode 100755 src/main/res/drawable-mdpi/ic_scan.png create mode 100755 src/main/res/drawable-mdpi/ic_settings.png create mode 100755 src/main/res/drawable-mdpi/ic_star_empty.png create mode 100755 src/main/res/drawable-mdpi/ic_star_filled.png create mode 100755 src/main/res/drawable-mdpi/ic_tag_black.png create mode 100755 src/main/res/drawable-mdpi/ic_tag_blue.png create mode 100755 src/main/res/drawable-mdpi/ic_upload.png delete mode 100644 src/main/res/drawable-mdpi/icon.png create mode 100755 src/main/res/drawable-xhdpi/ic_action_content_new.png create mode 100755 src/main/res/drawable-xhdpi/ic_browse.png create mode 100755 src/main/res/drawable-xhdpi/ic_collection_black.png create mode 100755 src/main/res/drawable-xhdpi/ic_collection_blue.png create mode 100755 src/main/res/drawable-xhdpi/ic_delete.png create mode 100755 src/main/res/drawable-xhdpi/ic_down.png create mode 100755 src/main/res/drawable-xhdpi/ic_edit.png create mode 100755 src/main/res/drawable-xhdpi/ic_favorites_black.png create mode 100755 src/main/res/drawable-xhdpi/ic_favorites_blue.png create mode 100755 src/main/res/drawable-xhdpi/ic_isbn.png create mode 100755 src/main/res/drawable-xhdpi/ic_item_black.png create mode 100755 src/main/res/drawable-xhdpi/ic_item_blue.png create mode 100755 src/main/res/drawable-xhdpi/ic_launcher.png create mode 100755 src/main/res/drawable-xhdpi/ic_manual.png create mode 100755 src/main/res/drawable-xhdpi/ic_plus.png create mode 100755 src/main/res/drawable-xhdpi/ic_settings.png create mode 100755 src/main/res/drawable-xhdpi/ic_star_empty.png create mode 100755 src/main/res/drawable-xhdpi/ic_star_filled.png create mode 100755 src/main/res/drawable-xhdpi/ic_tag_black.png create mode 100755 src/main/res/drawable-xhdpi/ic_tag_blue.png create mode 100755 src/main/res/drawable-xhdpi/ic_upload.png create mode 100755 src/main/res/drawable-xxhdpi/ic_action_content_new.png create mode 100755 src/main/res/drawable-xxhdpi/ic_browse.png create mode 100755 src/main/res/drawable-xxhdpi/ic_collection_black.png create mode 100755 src/main/res/drawable-xxhdpi/ic_collection_blue.png create mode 100755 src/main/res/drawable-xxhdpi/ic_delete.png create mode 100755 src/main/res/drawable-xxhdpi/ic_down.png create mode 100755 src/main/res/drawable-xxhdpi/ic_edit.png create mode 100755 src/main/res/drawable-xxhdpi/ic_favorites_black.png create mode 100755 src/main/res/drawable-xxhdpi/ic_favorites_blue.png create mode 100755 src/main/res/drawable-xxhdpi/ic_isbn.png create mode 100755 src/main/res/drawable-xxhdpi/ic_item_black.png create mode 100755 src/main/res/drawable-xxhdpi/ic_item_blue.png create mode 100755 src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100755 src/main/res/drawable-xxhdpi/ic_manual.png create mode 100755 src/main/res/drawable-xxhdpi/ic_plus.png create mode 100755 src/main/res/drawable-xxhdpi/ic_scan.png create mode 100755 src/main/res/drawable-xxhdpi/ic_settings.png create mode 100755 src/main/res/drawable-xxhdpi/ic_star_empty.png create mode 100755 src/main/res/drawable-xxhdpi/ic_star_filled.png create mode 100755 src/main/res/drawable-xxhdpi/ic_tag_black.png create mode 100755 src/main/res/drawable-xxhdpi/ic_tag_blue.png create mode 100755 src/main/res/drawable-xxhdpi/ic_upload.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_action_content_new.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_browse.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_collection_black.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_collection_blue.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_delete.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_down.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_edit.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_favorites_black.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_favorites_blue.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_isbn.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_item_black.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_item_blue.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_launcher.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_manual.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_plus.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_scan.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_settings.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_star_empty.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_star_filled.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_tag_black.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_tag_blue.png create mode 100755 src/main/res/drawable-xxxhdpi/ic_upload.png create mode 100755 src/main/res/drawable/bg_light.xml create mode 100755 src/main/res/drawable/bg_primary_light.xml mode change 100644 => 100755 src/main/res/drawable/book.png mode change 100644 => 100755 src/main/res/drawable/book_open.png mode change 100644 => 100755 src/main/res/drawable/comment.png mode change 100644 => 100755 src/main/res/drawable/email.png create mode 100755 src/main/res/drawable/fab_label_background.xml mode change 100644 => 100755 src/main/res/drawable/film.png mode change 100644 => 100755 src/main/res/drawable/folder.png delete mode 100644 src/main/res/drawable/glyphish_02_redo.png delete mode 100644 src/main/res/drawable/glyphish_104_index_cards.png delete mode 100644 src/main/res/drawable/glyphish_106_sliders.png delete mode 100644 src/main/res/drawable/glyphish_10_medical.png delete mode 100644 src/main/res/drawable/glyphish_151_telescope.png delete mode 100644 src/main/res/drawable/glyphish_195_barcode.png delete mode 100644 src/main/res/drawable/glyphish_22_skull_n_bones.png delete mode 100644 src/main/res/drawable/glyphish_59_flag.png create mode 100755 src/main/res/drawable/ic_action_content_new.png mode change 100644 => 100755 src/main/res/drawable/layout.png mode change 100644 => 100755 src/main/res/drawable/list_child_indicator.xml mode change 100644 => 100755 src/main/res/drawable/map.png mode change 100644 => 100755 src/main/res/drawable/newspaper.png mode change 100644 => 100755 src/main/res/drawable/note.png mode change 100644 => 100755 src/main/res/drawable/page.png mode change 100644 => 100755 src/main/res/drawable/page_white.png mode change 100644 => 100755 src/main/res/drawable/page_white_acrobat.png mode change 100644 => 100755 src/main/res/drawable/page_white_powerpoint.png mode change 100644 => 100755 src/main/res/drawable/page_white_text.png mode change 100644 => 100755 src/main/res/drawable/page_white_text_width.png mode change 100644 => 100755 src/main/res/drawable/page_white_width.png mode change 100644 => 100755 src/main/res/drawable/picture.png mode change 100644 => 100755 src/main/res/drawable/report.png mode change 100644 => 100755 src/main/res/drawable/report_user.png mode change 100644 => 100755 src/main/res/drawable/script.png create mode 100755 src/main/res/drawable/state_list_item.xml mode change 100644 => 100755 src/main/res/drawable/television.png delete mode 100644 src/main/res/drawable/zandy72.png create mode 100755 src/main/res/layout-sw600dp/collections.xml create mode 100755 src/main/res/layout/activity_main.xml create mode 100755 src/main/res/layout/activity_pdf_handler.xml create mode 100755 src/main/res/layout/coll_mem_activity.xml mode change 100644 => 100755 src/main/res/layout/collections.xml create mode 100755 src/main/res/layout/collections_old.xml create mode 100755 src/main/res/layout/creator_activity.xml mode change 100644 => 100755 src/main/res/layout/creator_dialog.xml create mode 100755 src/main/res/layout/fragment_collections.xml create mode 100755 src/main/res/layout/fragment_favorites.xml create mode 100755 src/main/res/layout/fragment_items.xml create mode 100755 src/main/res/layout/fragment_tags.xml mode change 100644 => 100755 src/main/res/layout/items.xml mode change 100644 => 100755 src/main/res/layout/json.xml mode change 100644 => 100755 src/main/res/layout/list_attach.xml create mode 100755 src/main/res/layout/list_coll_mem.xml mode change 100644 => 100755 src/main/res/layout/list_collection.xml mode change 100644 => 100755 src/main/res/layout/list_data.xml mode change 100644 => 100755 src/main/res/layout/list_item.xml create mode 100755 src/main/res/layout/list_tag.xml mode change 100644 => 100755 src/main/res/layout/lookup.xml mode change 100644 => 100755 src/main/res/layout/main.xml mode change 100644 => 100755 src/main/res/layout/note.xml create mode 100755 src/main/res/layout/preference_layout.xml delete mode 100644 src/main/res/layout/preferences.xml create mode 100755 src/main/res/layout/preferences_old.xml mode change 100644 => 100755 src/main/res/layout/requests.xml create mode 100755 src/main/res/layout/search_activity.xml create mode 100755 src/main/res/layout/tag_activity.xml create mode 100755 src/main/res/layout/tag_items.xml create mode 100755 src/main/res/menu/menu_activity__preference.xml mode change 100644 => 100755 src/main/res/menu/note_menu.xml mode change 100644 => 100755 src/main/res/menu/zotero_menu.xml create mode 100755 src/main/res/menu/zzz.xml mode change 100644 => 100755 src/main/res/values-en/strings.xml mode change 100644 => 100755 src/main/res/values-es/strings.xml create mode 100755 src/main/res/values-fr/drawable-hdpi/ic_favorites_blue.png create mode 100755 src/main/res/values-fr/drawable-hdpi/ic_tag_blue.png create mode 100755 src/main/res/values-fr/drawable-mdpi/ic_favorites_blue.png create mode 100755 src/main/res/values-fr/drawable-mdpi/ic_tag_blue.png create mode 100755 src/main/res/values-fr/drawable-xhdpi/ic_favorites_blue.png create mode 100755 src/main/res/values-fr/drawable-xhdpi/ic_tag_blue.png create mode 100755 src/main/res/values-fr/drawable-xxhdpi/ic_favorites_blue.png create mode 100755 src/main/res/values-fr/drawable-xxhdpi/ic_tag_blue.png create mode 100755 src/main/res/values-fr/drawable-xxxhdpi/ic_favorites_blue.png create mode 100755 src/main/res/values-fr/drawable-xxxhdpi/ic_tag_blue.png mode change 100644 => 100755 src/main/res/values-fr/strings.xml mode change 100644 => 100755 src/main/res/values-pt/strings.xml mode change 100644 => 100755 src/main/res/values-ru/strings.xml create mode 100755 src/main/res/values-w820dp/dimens.xml create mode 100755 src/main/res/values/colors.xml create mode 100755 src/main/res/values/dimens.xml mode change 100644 => 100755 src/main/res/values/strings.xml create mode 100755 src/main/res/values/styles.xml create mode 100755 src/main/res/xml/drawable-hdpi/ic_favorites_black.png create mode 100755 src/main/res/xml/drawable-mdpi/ic_favorites_black.png create mode 100755 src/main/res/xml/drawable-xhdpi/ic_favorites_black.png create mode 100755 src/main/res/xml/drawable-xxhdpi/ic_favorites_black.png create mode 100755 src/main/res/xml/drawable-xxxhdpi/ic_favorites_black.png mode change 100644 => 100755 src/main/res/xml/searchable.xml mode change 100644 => 100755 src/main/res/xml/settings.xml diff --git a/build.gradle b/build.gradle index 7c63de8..22c1939 100644 --- a/build.gradle +++ b/build.gradle @@ -3,11 +3,9 @@ import groovy.swing.SwingBuilder buildscript { repositories { mavenCentral() - maven { url 'http://download.crashlytics.com/maven' } } dependencies { - classpath 'com.android.tools.build:gradle:0.12.+' - classpath 'com.crashlytics.tools.gradle:crashlytics-gradle:1.+' + classpath 'com.android.tools.build:gradle:1.2.3' } } @@ -15,42 +13,43 @@ buildscript { repositories { mavenLocal() mavenCentral() - maven { url 'http://download.crashlytics.com/maven' } + jcenter() } apply plugin: 'android' -apply plugin: 'crashlytics' dependencies { + compile 'com.android.support:support-v4:21.0.+' + compile 'com.android.support:appcompat-v7:21.0.+' compile 'oauth.signpost:signpost-commonshttp4:1.2.1.2' compile 'com.intellij:annotations:12.0' - compile 'com.crashlytics.android:crashlytics:1.+' compile 'commons-io:commons-io:2.4' compile 'com.squareup:otto:1.3.4' compile 'com.google.zxing:android-integration:2.3.0' - -// compile project(":libzotero-java") + compile 'com.getbase:floatingactionbutton:1.10.0' + compile 'com.nononsenseapps:filepicker:2.2.3' } android { - compileSdkVersion 19 - buildToolsVersion "20.0.0" + compileSdkVersion 22 + buildToolsVersion "22.0.1" defaultConfig { + minSdkVersion 16 + targetSdkVersion 22 versionCode 1440 versionName "1.4.4" } productFlavors { - amazon google } signingConfigs { release { storeFile file("keystore") - keyAlias "zandy" + keyAlias "zotable" storePassword "" keyPassword "" } @@ -58,9 +57,9 @@ android { buildTypes { release { - zipAlign true + zipAlignEnabled true signingConfig signingConfigs.release - runProguard false + minifyEnabled false proguardFile getDefaultProguardFile('proguard-android.txt') } } @@ -77,7 +76,7 @@ android { gradle.taskGraph.whenReady { taskGraph -> println taskGraph.allTasks - if(taskGraph.hasTask(':zandy:assembleGoogleRelease')) { + if(taskGraph.hasTask(':zotable:assembleGoogleRelease')) { def storePass = '' def keyPass = '' diff --git a/src/instrumentTest/java/com/gimranov/zandy/app/test/ApiTest.java b/src/instrumentTest/java/com/gimranov/zandy/app/test/ApiTest.java deleted file mode 100644 index 19d10c4..0000000 --- a/src/instrumentTest/java/com/gimranov/zandy/app/test/ApiTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.gimranov.zandy.app.test; - -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.test.AndroidTestCase; -import android.test.IsolatedContext; - -import com.gimranov.zandy.app.ServerCredentials; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.task.APIException; -import com.gimranov.zandy.app.task.APIRequest; - - -public class ApiTest extends AndroidTestCase { - - private IsolatedContext mContext; - private Database mDb; - private ServerCredentials mCred; - - /** - * Access information for the Zandy test user on Zotero.org - */ - private static final String TEST_UID = "743083"; - private static final String TEST_KEY = "JFRP2k4qvhRUm62kuDHXUUX3"; - private static final String TEST_COLLECTION = "U8GNSSF3"; - - // unlikely to exist - private static final String TEST_MISSING_ITEM = "ZZZZZZZZ"; - - - @Override - protected void setUp() throws Exception { - super.setUp(); - mContext = new IsolatedContext(null, getContext()); - mDb = new Database(mContext); - - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mContext); - SharedPreferences.Editor editor = settings.edit(); - // For Zotero, the key and secret are identical, it seems - editor.putString("user_key", TEST_KEY); - editor.putString("user_secret", TEST_KEY); - editor.putString("user_id", TEST_UID); - editor.commit(); - - mCred = new ServerCredentials(mContext); - } - - public void testPreConditions() { - // Make sure we do indeed have the key set up - assertTrue(ServerCredentials.check(mContext)); - } - - public void testItemsRequest() throws APIException { - APIRequest items = APIRequest.fetchItems(false, mCred); - items.issue(mDb, mCred); - } - - public void testCollectionsRequest() throws APIException { - APIRequest collections = APIRequest.fetchCollections(mCred); - collections.issue(mDb, mCred); - } - - public void testItemsForCollection() throws APIException { - APIRequest collection = APIRequest.fetchItems(TEST_COLLECTION, false, mCred); - collection.issue(mDb, mCred); - } - - // verify that we fail on this item, which should be missing - public void testMissingItem() throws APIException { - APIRequest missingItem = APIRequest.fetchItem(TEST_MISSING_ITEM, mCred); - try { - missingItem.issue(mDb, mCred); - // We shouldn't get here - assertTrue(false); - } catch (APIException e) { - // We expect only one specific exception message - if (!"Item does not exist".equals(e.getMessage())) - throw e; - } - } -} diff --git a/src/instrumentTest/java/com/gimranov/zandy/app/test/MainTest.java b/src/instrumentTest/java/com/gimranov/zandy/app/test/MainTest.java deleted file mode 100644 index 7cfc14e..0000000 --- a/src/instrumentTest/java/com/gimranov/zandy/app/test/MainTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.gimranov.zandy.app.test; - -import android.test.ActivityInstrumentationTestCase2; -import android.widget.Button; - -import com.gimranov.zandy.app.MainActivity; - -public class MainTest extends ActivityInstrumentationTestCase2 { - - private MainActivity mActivity; - private Button loginButton; - - public MainTest() { - super("com.gimranov.zandy.app", MainActivity.class); - } - - public void testPreconditions() { - assertNotNull(loginButton); - } - - @Override - protected void setUp() throws Exception { - super.setUp(); - mActivity = this.getActivity(); - loginButton = (Button) mActivity.findViewById(com.gimranov.zandy.app.R.id.loginButton); - } -} diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml old mode 100644 new mode 100755 index e966093..778771a --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -1,33 +1,35 @@ + package="com.mattrobertson.zotable.app" > + android:minSdkVersion="16" + android:targetSdkVersion="22" /> + + + android:name=".Application" + android:icon="@drawable/ic_launcher" + android:label="@string/app_name" + android:theme="@style/AppTheme" > + android:launchMode="singleInstance" > + - - - + @@ -39,13 +41,26 @@ android:name=".CollectionActivity" android:label="@string/app_name" android:launchMode="standard" /> + + + + + + + + android:launchMode="standard" > + @@ -75,20 +90,44 @@ android:label="@string/app_name" android:launchMode="standard" /> + - + + + + + + + + + + + + + + + + + diff --git a/src/main/java/com/gimranov/zandy/app/Application.java b/src/main/java/com/gimranov/zandy/app/Application.java deleted file mode 100644 index 618ffc6..0000000 --- a/src/main/java/com/gimranov/zandy/app/Application.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.gimranov.zandy.app; - -import com.squareup.otto.Bus; - -public class Application extends android.app.Application { - private static final String TAG = Application.class.getCanonicalName(); - - private static Application instance; - - private Bus bus; - - @Override - public void onCreate() { - super.onCreate(); - - bus = new Bus(); - - instance = this; - } - - public Bus getBus() { - return bus; - } - - public static Application getInstance() { - return instance; - } -} diff --git a/src/main/java/com/gimranov/zandy/app/CollectionActivity.java b/src/main/java/com/gimranov/zandy/app/CollectionActivity.java deleted file mode 100644 index 87f178b..0000000 --- a/src/main/java/com/gimranov/zandy/app/CollectionActivity.java +++ /dev/null @@ -1,310 +0,0 @@ -/******************************************************************************* - * This file is part of Zandy. - * - * Zandy is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Zandy is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . - ******************************************************************************/ -package com.gimranov.zandy.app; - -import android.app.ListActivity; -import android.content.Intent; -import android.database.Cursor; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.AdapterView.OnItemLongClickListener; -import android.widget.ListView; -import android.widget.TextView; -import android.widget.Toast; - -import com.gimranov.zandy.app.data.CollectionAdapter; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.data.ItemCollection; -import com.gimranov.zandy.app.task.APIEvent; -import com.gimranov.zandy.app.task.APIRequest; -import com.gimranov.zandy.app.task.ZoteroAPITask; - -public class CollectionActivity extends ListActivity { - - private static final String TAG = "com.gimranov.zandy.app.CollectionActivity"; - private ItemCollection collection; - private Database db; - - final Handler handler = new Handler() { - public void handleMessage(Message msg) { - Log.d(TAG,"received message: "+msg.arg1); - refreshView(); - - if (msg.arg1 == APIRequest.UPDATED_DATA) { - //refreshView(); - return; - } - - if (msg.arg1 == APIRequest.QUEUED_MORE) { - Toast.makeText(getApplicationContext(), - getResources().getString(R.string.sync_queued_more, msg.arg2), - Toast.LENGTH_SHORT).show(); - return; - } - - if (msg.arg1 == APIRequest.BATCH_DONE) { - Application.getInstance().getBus().post(SyncEvent.COMPLETE); - Toast.makeText(getApplicationContext(), - getResources().getString(R.string.sync_complete), - Toast.LENGTH_SHORT).show(); - return; - } - - if (msg.arg1 == APIRequest.ERROR_UNKNOWN) { - String desc = (msg.arg2 == 0) ? "" : " ("+msg.arg2+")"; - Toast.makeText(getApplicationContext(), - getResources().getString(R.string.sync_error)+desc, - Toast.LENGTH_SHORT).show(); - return; - } - } - }; - - /** - * Refreshes the current list adapter - */ - private void refreshView() { - CollectionAdapter adapter = (CollectionAdapter) getListAdapter(); - Cursor newCursor = (collection == null) ? create() : create(collection); - adapter.changeCursor(newCursor); - adapter.notifyDataSetChanged(); - Log.d(TAG, "refreshing view on request"); - } - - /** Called when the activity is first created. */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - db = new Database(this); - - setContentView(R.layout.collections); - - CollectionAdapter collectionAdapter; - - String collectionKey = getIntent().getStringExtra("com.gimranov.zandy.app.collectionKey"); - if (collectionKey != null) { - ItemCollection coll = ItemCollection.load(collectionKey, db); - // We set the title to the current collection - this.collection = coll; - this.setTitle(coll.getTitle()); - collectionAdapter = new CollectionAdapter(this, create(coll)); - } else { - this.setTitle(getResources().getString(R.string.collections)); - collectionAdapter = new CollectionAdapter(this, create()); - } - - setListAdapter(collectionAdapter); - - ListView lv = getListView(); - lv.setOnItemLongClickListener(new OnItemLongClickListener() { - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { - CollectionAdapter adapter = (CollectionAdapter) parent.getAdapter(); - Cursor cur = adapter.getCursor(); - // Place the cursor at the selected item - if (cur.moveToPosition(position)) { - // and open activity for the selected collection - ItemCollection coll = ItemCollection.load(cur); - if (coll != null && coll.getKey() != null && coll.getSubcollections(db).size() > 0) { - Log.d(TAG, "Loading child collection with key: "+coll.getKey()); - // We create and issue a specified intent with the necessary data - Intent i = new Intent(getBaseContext(), CollectionActivity.class); - i.putExtra("com.gimranov.zandy.app.collectionKey", coll.getKey()); - startActivity(i); - } else { - Log.d(TAG, "Failed loading child collections for collection"); - Toast.makeText(getApplicationContext(), - getResources().getString(R.string.collection_no_subcollections), - Toast.LENGTH_SHORT).show(); - } - } else { - // failed to move cursor-- show a toast - TextView tvTitle = (TextView)view.findViewById(R.id.collection_title); - Toast.makeText(getApplicationContext(), - getResources().getString(R.string.collection_cant_open, tvTitle.getText()), - Toast.LENGTH_SHORT).show(); - } - return true; - } - }); - - lv.setOnItemClickListener(new OnItemClickListener() { - public void onItemClick(AdapterView parent, View view, int position, long id) { - - CollectionAdapter adapter = (CollectionAdapter) parent.getAdapter(); - Cursor cur = adapter.getCursor(); - // Place the cursor at the selected item - if (cur.moveToPosition(position)) { - // and replace the cursor with one for the selected collection - ItemCollection coll = ItemCollection.load(cur); - if (coll != null && coll.getKey() != null) { - Intent i = new Intent(getBaseContext(), ItemActivity.class); - if (coll.getSize() == 0) { - // Send a message that we need to refresh the collection - i.putExtra("com.gimranov.zandy.app.rerequest", true); - } - i.putExtra("com.gimranov.zandy.app.collectionKey", coll.getKey()); - startActivity(i); - } else { - // collection loaded was null. why? - Log.d(TAG, "Failed loading items for collection at position: "+position); - return; - } - } else { - // failed to move cursor-- show a toast - TextView tvTitle = (TextView)view.findViewById(R.id.collection_title); - Toast.makeText(getApplicationContext(), - getResources().getString(R.string.collection_cant_open, tvTitle.getText()), - Toast.LENGTH_SHORT).show(); - return; - } - return; - } - }); - } - - protected void onResume() { - CollectionAdapter adapter = (CollectionAdapter) getListAdapter(); - // XXX This may be too agressive-- fix if causes issues - Cursor newCursor = (collection == null) ? create() : create(collection); - adapter.changeCursor(newCursor); - adapter.notifyDataSetChanged(); - if (db == null) db = new Database(this); - super.onResume(); - } - - public void onDestroy() { - CollectionAdapter adapter = (CollectionAdapter) getListAdapter(); - Cursor cur = adapter.getCursor(); - if(cur != null) cur.close(); - if (db != null) db.close(); - super.onDestroy(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.zotero_menu, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle item selection - switch (item.getItemId()) { - case R.id.do_sync: - if (!ServerCredentials.check(getApplicationContext())) { - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_log_in_first), - Toast.LENGTH_SHORT).show(); - return true; - } - APIRequest req = APIRequest.fetchCollections(new ServerCredentials(getApplicationContext())); - req.setHandler(new APIEvent() { - private int updates = 0; - - @Override - public void onComplete(APIRequest request) { - Message msg = handler.obtainMessage(); - msg.arg1 = APIRequest.BATCH_DONE; - handler.sendMessage(msg); - Log.d(TAG, "fired oncomplete"); - } - - @Override - public void onUpdate(APIRequest request) { - updates++; - - Message msg = handler.obtainMessage(); - msg.arg1 = APIRequest.UPDATED_DATA; - handler.sendMessage(msg); - } - - @Override - public void onError(APIRequest request, Exception exception) { - Log.e(TAG, "APIException caught", exception); - Message msg = handler.obtainMessage(); - msg.arg1 = APIRequest.ERROR_UNKNOWN; - handler.sendMessage(msg); - } - - @Override - public void onError(APIRequest request, int error) { - Log.e(TAG, "API error caught"); - Message msg = handler.obtainMessage(); - msg.arg1 = APIRequest.ERROR_UNKNOWN; - msg.arg2 = request.status; - handler.sendMessage(msg); - } - }); - ZoteroAPITask task = new ZoteroAPITask(getBaseContext()); - task.setHandler(handler); - task.execute(req); - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_collection), - Toast.LENGTH_SHORT).show(); - return true; - case R.id.do_new: - Log.d(TAG, "Can't yet make new collections"); - // XXX no i18n for temporary string - Toast.makeText(getApplicationContext(), "Sorry, new collection creation is not yet possible. Soon!", - Toast.LENGTH_SHORT).show(); - return true; - case R.id.do_prefs: - startActivity(new Intent(this, SettingsActivity.class)); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - /** - * Gives a cursor for top-level collections - * @return - */ - public Cursor create() { - String[] args = { "false" }; - Cursor cursor = db.query("collections", Database.COLLCOLS, "collection_parent=?", args, null, null, "collection_name", null); - if (cursor == null) { - Log.e(TAG, "cursor is null"); - } - - return cursor; - } - - /** - * Gives a cursor for child collections of a given parent - * @param parent - * @return - */ - public Cursor create(ItemCollection parent) { - String[] args = { parent.getKey() }; - Cursor cursor = db.query("collections", Database.COLLCOLS, "collection_parent=?", args, null, null, "collection_name", null); - if (cursor == null) { - Log.e(TAG, "cursor is null"); - } - - return cursor; - } - -} diff --git a/src/main/java/com/gimranov/zandy/app/CollectionMembershipActivity.java b/src/main/java/com/gimranov/zandy/app/CollectionMembershipActivity.java deleted file mode 100644 index 7fa3657..0000000 --- a/src/main/java/com/gimranov/zandy/app/CollectionMembershipActivity.java +++ /dev/null @@ -1,249 +0,0 @@ -/******************************************************************************* - * This file is part of Zandy. - * - * Zandy is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Zandy is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . - ******************************************************************************/ -package com.gimranov.zandy.app; - -import java.util.ArrayList; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.ListActivity; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.TextView; -import android.widget.Toast; - -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.data.Item; -import com.gimranov.zandy.app.data.ItemCollection; -import com.gimranov.zandy.app.task.APIRequest; -import com.gimranov.zandy.app.task.ZoteroAPITask; - -/** - * This Activity handles displaying and editing collection memberships for a - * given item. - * - * @author ajlyon - * - */ -public class CollectionMembershipActivity extends ListActivity { - - private static final String TAG = "com.gimranov.zandy.app.CollectionMembershipActivity"; - - static final int DIALOG_CONFIRM_NAVIGATE = 4; - static final int DIALOG_COLLECTION_LIST = 1; - - private String itemKey; - private String itemTitle; - - private Database db; - - /** - * For API <= 7, where we can't pass Bundles to dialogs - */ - private Bundle b = new Bundle(); - - /** Called when the activity is first created. */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - db = new Database(this); - - /* Get the incoming data from the calling activity */ - itemKey = getIntent().getStringExtra("com.gimranov.zandy.app.itemKey"); - Item item = Item.load(itemKey, db); - if (item == null) { - Log.e(TAG, "Null item for key: "+itemKey); - finish(); - } - itemTitle = item.getTitle(); - - this.setTitle(getResources().getString(R.string.collections_for_item, itemTitle)); - - ArrayList rows = ItemCollection.getCollections(item, db); - - setListAdapter(new ArrayAdapter(this, R.layout.list_data, rows) { - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View row; - - // We are reusing views, but we need to initialize it if null - if (null == convertView) { - LayoutInflater inflater = getLayoutInflater(); - row = inflater.inflate(R.layout.list_data, null); - } else { - row = convertView; - } - - /* Our layout has just two fields */ - TextView tvLabel = (TextView) row.findViewById(R.id.data_label); - tvLabel.setText(""); - TextView tvContent = (TextView) row.findViewById(R.id.data_content); - - tvContent.setText(getItem(position).getTitle()); - - return row; - } - }); - - ListView lv = getListView(); - lv.setTextFilterEnabled(true); - lv.setOnItemClickListener(new OnItemClickListener() { - // Warning here because Eclipse can't tell whether my ArrayAdapter is - // being used with the correct parametrization. - @SuppressWarnings("unchecked") - public void onItemClick(AdapterView parent, View view, int position, long id) { - // If we have a click on an entry, prompt to view that tag's items. - ArrayAdapter adapter = (ArrayAdapter) parent.getAdapter(); - ItemCollection row = adapter.getItem(position); - Bundle b = new Bundle(); - b.putString("itemKey", itemKey); - b.putString("collectionKey", row.getKey()); - CollectionMembershipActivity.this.b = b; - removeDialog(DIALOG_CONFIRM_NAVIGATE); - showDialog(DIALOG_CONFIRM_NAVIGATE); - } - }); - - } - - @Override - public void onDestroy() { - if (db != null) db.close(); - super.onDestroy(); - } - - @Override - public void onResume() { - if (db == null) db = new Database(this); - super.onResume(); - } - - protected Dialog onCreateDialog(int id) { - final String collectionKey = b.getString("collectionKey"); - final String itemKey = b.getString("itemKey"); - AlertDialog dialog; - - switch (id) { - case DIALOG_COLLECTION_LIST: - AlertDialog.Builder builder = new AlertDialog.Builder(this); - final ArrayList collections = ItemCollection.getCollections(db); - int size = collections.size(); - String[] collectionNames = new String[size]; - for (int i = 0; i < size; i++) { - collectionNames[i] = collections.get(i).getTitle(); - } - builder.setTitle(getResources().getString(R.string.choose_parent_collection)) - .setItems(collectionNames, new DialogInterface.OnClickListener() { - @SuppressWarnings("unchecked") - public void onClick(DialogInterface dialog, int pos) { - Item item = Item.load(itemKey, db); - collections.get(pos).add(item, false, db); - collections.get(pos).saveChildren(db); - ArrayAdapter la = (ArrayAdapter) getListAdapter(); - la.clear(); - for (ItemCollection b : ItemCollection.getCollections(item,db)) { - la.add(b); - } - la.notifyDataSetChanged(); - } - }); - dialog = builder.create(); - return dialog; - case DIALOG_CONFIRM_NAVIGATE: - dialog = new AlertDialog.Builder(this) - .setTitle(getResources().getString(R.string.collection_membership_detail)) - .setPositiveButton(getResources().getString(R.string.tag_view), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - Intent i = new Intent(getBaseContext(), ItemActivity.class); - i.putExtra("com.gimranov.zandy.app.collectionKey", collectionKey); - startActivity(i); - } - }).setNeutralButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - // do nothing - } - }).setNegativeButton(getResources().getString(R.string.collection_remove_item), new DialogInterface.OnClickListener() { - @SuppressWarnings("unchecked") - public void onClick(DialogInterface dialog, int whichButton) { - Item item = Item.load(itemKey, db); - ItemCollection coll = ItemCollection.load(collectionKey, db); - coll.remove(item, false, db); - ArrayAdapter la = (ArrayAdapter) getListAdapter(); - la.clear(); - for (ItemCollection b : ItemCollection.getCollections(item,db)) { - la.add(b); - } - la.notifyDataSetChanged(); - } - }).create(); - return dialog; - default: - Log.e(TAG, "Invalid dialog requested"); - return null; - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.zotero_menu, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle item selection - switch (item.getItemId()) { - case R.id.do_sync: - if (!ServerCredentials.check(getApplicationContext())) { - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_log_in_first), - Toast.LENGTH_SHORT).show(); - return true; - } - Log.d(TAG, "Preparing sync requests"); - new ZoteroAPITask(getBaseContext()).execute(APIRequest.update(Item.load(itemKey, db))); - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_started), - Toast.LENGTH_SHORT).show(); - return true; - case R.id.do_new: - Bundle b = new Bundle(); - b.putString("itemKey", itemKey); - removeDialog(DIALOG_COLLECTION_LIST); - this.b = b; - showDialog(DIALOG_COLLECTION_LIST); - return true; - case R.id.do_prefs: - startActivity(new Intent(this, SettingsActivity.class)); - return true; - default: - return super.onOptionsItemSelected(item); - } - } -} diff --git a/src/main/java/com/gimranov/zandy/app/CreatorActivity.java b/src/main/java/com/gimranov/zandy/app/CreatorActivity.java deleted file mode 100644 index 4406fb5..0000000 --- a/src/main/java/com/gimranov/zandy/app/CreatorActivity.java +++ /dev/null @@ -1,381 +0,0 @@ -/******************************************************************************* - * This file is part of Zandy. - * - * Zandy is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Zandy is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . - ******************************************************************************/ -package com.gimranov.zandy.app; - -import java.util.ArrayList; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.ListActivity; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.AdapterView.OnItemLongClickListener; -import android.widget.ArrayAdapter; -import android.widget.CheckBox; -import android.widget.ListView; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.Toast; - -import com.gimranov.zandy.app.data.Creator; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.data.Item; -import com.gimranov.zandy.app.task.APIRequest; -import com.gimranov.zandy.app.task.ZoteroAPITask; - -/** - * This Activity handles displaying and editing creators. It works almost the same as - * ItemDataActivity and TagActivity, using a simple ArrayAdapter on Bundles with the creator info. - * - * This currently operates by showing the creators for a given item; it could be - * modified some day to show all creators in the database (when they come to be saved - * that way). - * - * @author ajlyon - * - */ -public class CreatorActivity extends ListActivity { - - private static final String TAG = "com.gimranov.zandy.app.CreatorActivity"; - - static final int DIALOG_CREATOR = 3; - static final int DIALOG_CONFIRM_NAVIGATE = 4; - static final int DIALOG_CONFIRM_DELETE = 5; - - public Item item; - - private Database db; - - /** - * For API <= 7, to pass bundles to activities - */ - private Bundle b = new Bundle(); - - /** Called when the activity is first created. */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - db = new Database(this); - - /* Get the incoming data from the calling activity */ - String itemKey = getIntent().getStringExtra("com.gimranov.zandy.app.itemKey"); - Item item = Item.load(itemKey, db); - this.item = item; - - this.setTitle("Creators for "+item.getTitle()); - - ArrayList rows = item.creatorsToBundleArray(); - - /* - * We use the standard ArrayAdapter, passing in our data as a Bundle. - * Since it's no longer a simple TextView, we need to override getView, but - * we can do that anonymously. - */ - setListAdapter(new ArrayAdapter(this, R.layout.list_data, rows) { - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View row; - - // We are reusing views, but we need to initialize it if null - if (null == convertView) { - LayoutInflater inflater = getLayoutInflater(); - row = inflater.inflate(R.layout.list_data, null); - } else { - row = convertView; - } - - /* Our layout has just two fields */ - TextView tvLabel = (TextView) row.findViewById(R.id.data_label); - TextView tvContent = (TextView) row.findViewById(R.id.data_content); - - tvLabel.setText(Item.localizedStringForString( - getItem(position).getString("creatorType"))); - tvContent.setText(getItem(position).getString("name")); - - return row; - } - }); - - ListView lv = getListView(); - lv.setTextFilterEnabled(true); - lv.setOnItemClickListener(new OnItemClickListener() { - // Warning here because Eclipse can't tell whether my ArrayAdapter is - // being used with the correct parametrization. - @SuppressWarnings("unchecked") - public void onItemClick(AdapterView parent, View view, int position, long id) { - // If we have a click on an entry, do something... - ArrayAdapter adapter = (ArrayAdapter) parent.getAdapter(); - Bundle row = adapter.getItem(position); - -/* TODO Rework this logic to open an ItemActivity showing items with this creator - if (row.getString("label").equals("url")) { - row.putString("url", row.getString("content")); - removeDialog(DIALOG_CONFIRM_NAVIGATE); - showDialog(DIALOG_CONFIRM_NAVIGATE, row); - return; - } - - if (row.getString("label").equals("DOI")) { - String url = "http://dx.doi.org/"+Uri.encode(row.getString("content")); - row.putString("url", url); - removeDialog(DIALOG_CONFIRM_NAVIGATE); - showDialog(DIALOG_CONFIRM_NAVIGATE, row); - return; - } - */ - Toast.makeText(getApplicationContext(), row.getString("name"), - Toast.LENGTH_SHORT).show(); - } - }); - - /* - * On long click, we bring up an edit dialog. - */ - lv.setOnItemLongClickListener(new OnItemLongClickListener() { - /* - * Same annotation as in onItemClick(..), above. - */ - @SuppressWarnings("unchecked") - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { - // If we have a long click on an entry, show an editor - ArrayAdapter adapter = (ArrayAdapter) parent.getAdapter(); - Bundle row = adapter.getItem(position); - CreatorActivity.this.b = row; - removeDialog(DIALOG_CREATOR); - showDialog(DIALOG_CREATOR); - return true; - } - }); - - } - - @Override - public void onDestroy() { - if (db != null) db.close(); - super.onDestroy(); - } - - @Override - public void onResume() { - if (db == null) db = new Database(this); - super.onResume(); - } - - protected Dialog onCreateDialog(int id) { - final String creatorType = b.getString("creatorType"); - final int creatorPosition = b.getInt("position"); - - String name = b.getString("name"); - String firstName = b.getString("firstName"); - String lastName = b.getString("lastName"); - - switch (id) { - /* Editor for a creator - */ - case DIALOG_CREATOR: - AlertDialog.Builder builder; - AlertDialog dialog; - - LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); - final View layout = inflater.inflate(R.layout.creator_dialog, - (ViewGroup) findViewById(R.id.layout_root)); - - TextView textName = (TextView) layout.findViewById(R.id.creator_name); - textName.setText(name); - TextView textFN = (TextView) layout.findViewById(R.id.creator_firstName); - textFN.setText(firstName); - TextView textLN = (TextView) layout.findViewById(R.id.creator_lastName); - textLN.setText(lastName); - - CheckBox mode = (CheckBox) layout.findViewById(R.id.creator_mode); - mode.setChecked((firstName == null || firstName.equals("")) - && (lastName == null || lastName.equals("")) - && (lastName != null && !name.equals(""))); - - // Set up the adapter to get creator types - String[] types = Item.localizedCreatorTypesForItemType(item.getType()); - - // what position are we? - int arrPosition = 0; - String localType = ""; - if (creatorType != null) { - localType = Item.localizedStringForString(creatorType); - } else { - // We default to the first possibility when none specified - localType = Item.localizedStringForString( - Item.creatorTypesForItemType(item.getType())[0]); - } - for (int i = 0; i < types.length; i++) { - if (types[i].equals(localType)) { - arrPosition = i; - break; - } - } - - ArrayAdapter adapter = new ArrayAdapter(this, - android.R.layout.simple_spinner_item, types); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - - Spinner spinner = (Spinner) layout.findViewById(R.id.creator_type); - spinner.setAdapter(adapter); - - spinner.setSelection(arrPosition); - builder = new AlertDialog.Builder(this); - builder.setView(layout); - builder.setPositiveButton(getResources().getString(R.string.ok), new OnClickListener(){ - @SuppressWarnings("unchecked") - public void onClick(DialogInterface dialog, int whichButton) { - Creator c; - TextView textName = (TextView) layout.findViewById(R.id.creator_name); - TextView textFN = (TextView) layout.findViewById(R.id.creator_firstName); - TextView textLN = (TextView) layout.findViewById(R.id.creator_lastName); - Spinner spinner = (Spinner) layout.findViewById(R.id.creator_type); - CheckBox mode = (CheckBox) layout.findViewById(R.id.creator_mode); - - String selected = (String) spinner.getSelectedItem(); - // Set up the adapter to get creator types - String[] types = Item.localizedCreatorTypesForItemType(item.getType()); - - // what position are we? - int typePos = 0; - for (int i = 0; i < types.length; i++) { - if (types[i].equals(selected)) { - typePos = i; - break; - } - } - - String realType = Item.creatorTypesForItemType(item.getType())[typePos]; - - if (mode.isChecked()) - c = new Creator(realType, textName.getText().toString(), true); - else - c = new Creator(realType, textFN.getText().toString(), textLN.getText().toString()); - - Item.setCreator(item.getKey(), c, creatorPosition, db); - item = Item.load(item.getKey(), db); - ArrayAdapter la = (ArrayAdapter) getListAdapter(); - la.clear(); - for (Bundle b : item.creatorsToBundleArray()) { - la.add(b); - } - la.notifyDataSetChanged(); - } - }); - - builder.setNeutralButton(getResources().getString(R.string.cancel), new OnClickListener(){ - public void onClick(DialogInterface dialog, int whichButton) { - // do nothing - } - }); - - builder.setNegativeButton(getResources().getString(R.string.menu_delete), new OnClickListener(){ - @SuppressWarnings("unchecked") - public void onClick(DialogInterface dialog, int whichButton) { - Item.setCreator(item.getKey(), null, creatorPosition, db); - item = Item.load(item.getKey(), db); - ArrayAdapter la = (ArrayAdapter) getListAdapter(); - la.clear(); - for (Bundle b : item.creatorsToBundleArray()) { - la.add(b); - } - la.notifyDataSetChanged(); - } - }); - - dialog = builder.create(); - return dialog; - - case DIALOG_CONFIRM_NAVIGATE: -/* dialog = new AlertDialog.Builder(this) - .setTitle("View this online?") - .setPositiveButton("View", new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - // The behavior for invalid URIs might be nasty, but - // we'll cross that bridge if we come to it. - Uri uri = Uri.parse(content); - startActivity(new Intent(Intent.ACTION_VIEW) - .setData(uri)); - } - }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - // do nothing - } - }).create(); - return dialog;*/ - return null; - default: - Log.e(TAG, "Invalid dialog requested"); - return null; - } - } - - /* - * I've been just copying-and-pasting the options menu code from activity to activity. - * It needs to be reworked for some of these activities. - */ - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.zotero_menu, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle item selection - switch (item.getItemId()) { - case R.id.do_sync: - if (!ServerCredentials.check(getApplicationContext())) { - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_log_in_first), - Toast.LENGTH_SHORT).show(); - return true; - } - Log.d(TAG, "Preparing sync requests, starting with present item"); - new ZoteroAPITask(getBaseContext()).execute(APIRequest.update(this.item)); - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_started), - Toast.LENGTH_SHORT).show(); - - return true; - case R.id.do_new: - Bundle row = new Bundle(); - row.putInt("position", -1); - row.putString("itemKey", this.item.getKey()); - removeDialog(DIALOG_CREATOR); - this.b = row; - showDialog(DIALOG_CREATOR); - return true; - case R.id.do_prefs: - startActivity(new Intent(this, SettingsActivity.class)); - return true; - default: - return super.onOptionsItemSelected(item); - } - } -} diff --git a/src/main/java/com/gimranov/zandy/app/LookupActivity.java b/src/main/java/com/gimranov/zandy/app/LookupActivity.java deleted file mode 100644 index 936db73..0000000 --- a/src/main/java/com/gimranov/zandy/app/LookupActivity.java +++ /dev/null @@ -1,260 +0,0 @@ -/******************************************************************************* - * This file is part of Zandy. - * - * Zandy is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Zandy is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . - ******************************************************************************/ -package com.gimranov.zandy.app; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.net.URLConnection; - -import org.apache.http.util.ByteArrayBuffer; - -import android.app.Activity; -import android.app.Dialog; -import android.app.ProgressDialog; -import android.content.Intent; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.text.Editable; -import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.Button; -import android.widget.TextView; - -import com.gimranov.zandy.app.data.Database; - -/** - * Runs lookup routines to create new items - * @author ajlyon - * - */ -public class LookupActivity extends Activity implements OnClickListener { - - private static final String TAG = "com.gimranov.zandy.app.LookupActivity"; - - static final int DIALOG_PROGRESS = 6; - - private ProgressDialog mProgressDialog; - private ProgressThread progressThread; - private Database db; - - private Bundle b = new Bundle(); - - /** Called when the activity is first created. */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - db = new Database(this); - - /* Get the incoming data from the calling activity */ - final String identifier = getIntent().getStringExtra("com.gimranov.zandy.app.identifier"); - final String mode = getIntent().getStringExtra("com.gimranov.zandy.app.mode"); - - setContentView(R.layout.lookup); - - Button lookupButton = (Button) findViewById(R.id.lookupButton); - lookupButton.setOnClickListener(this); - - } - - /** - * Implementation of the OnClickListener interface, to handle button events. - * - * Note: When adding a button, it needs to be added here, but the - * ClickListener needs to be set in the main onCreate(..) as well. - */ - public void onClick(View v) { - Log.d(TAG, "Click on: " + v.getId()); - if (v.getId() == R.id.lookupButton) { - Log.d(TAG, "Trying to start search activity"); - TextView field = (TextView) findViewById(R.id.identifier); - Editable fieldContents = (Editable) field.getText(); - Bundle b = new Bundle(); - b.putString("mode", "isbn"); - b.putString("identifier", fieldContents.toString()); - this.b = b; - showDialog(DIALOG_PROGRESS); - } else { - Log.w(TAG, "Uncaught click on: " + v.getId()); - } - } - - - @Override - public void onDestroy() { - super.onDestroy(); - } - - @Override - public void onResume() { - if (db == null) db = new Database(this); - super.onResume(); - } - - protected Dialog onCreateDialog(int id) { - switch (id) { - case DIALOG_PROGRESS: - mProgressDialog = new ProgressDialog(this); - mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - mProgressDialog.setIndeterminate(true); - mProgressDialog.setMax(100); - return mProgressDialog; - default: - Log.e(TAG, "Invalid dialog requested"); - return null; - } - } - - protected void onPrepareDialog(int id, Dialog dialog) { - switch(id) { - case DIALOG_PROGRESS: - mProgressDialog.setProgress(0); - mProgressDialog.setMessage("Looking up item..."); - progressThread = new ProgressThread(handler, b); - progressThread.start(); - } - } - - - final Handler handler = new Handler() { - public void handleMessage(Message msg) { - if (ProgressThread.STATE_DONE == msg.arg2) { - if(mProgressDialog.isShowing()) - dismissDialog(DIALOG_PROGRESS); - // do something-- we're done. - return; - } - - if (ProgressThread.STATE_PARSING == msg.arg2) { - mProgressDialog.setMessage("Parsing item data..."); - return; - } - - int total = msg.arg1; - mProgressDialog.setProgress(total); - if (total >= 100) { - dismissDialog(DIALOG_PROGRESS); - progressThread.setState(ProgressThread.STATE_DONE); - } - } - }; - - private class ProgressThread extends Thread { - Handler mHandler; - Bundle arguments; - final static int STATE_DONE = 5; - final static int STATE_FETCHING = 1; - final static int STATE_PARSING = 6; - int mState; - - ProgressThread(Handler h, Bundle b) { - mHandler = h; - arguments = b; - } - - public void run() { - mState = STATE_FETCHING; - - // Setup - String identifier = arguments.getString("identifier"); - String mode = arguments.getString("mode"); - URL url; - String urlstring; - - if ("isbn".equals(mode)) { - if (identifier == null || identifier.equals("")) - identifier = "0674081250"; - urlstring = "http://xisbn.worldcat.org/webservices/xid/isbn/" - + identifier - + "?method=getMetadata&fl=*&format=json&count=1"; - } else { - urlstring = ""; - } - - try { - Log.d(TAG, "Fetching from: "+urlstring); - url = new URL(urlstring); - - /* Open a connection to that URL. */ - URLConnection ucon = url.openConnection(); - /* - * Define InputStreams to read from the URLConnection. - */ - InputStream is = ucon.getInputStream(); - BufferedInputStream bis = new BufferedInputStream(is, 16000); - - ByteArrayBuffer baf = new ByteArrayBuffer(50); - int current = 0; - - /* - * Read bytes to the Buffer until there is nothing more to read(-1). - */ - while (mState == STATE_FETCHING - && (current = bis.read()) != -1) { - baf.append((byte) current); - - if (baf.length() % 2048 == 0) { - Message msg = mHandler.obtainMessage(); - // XXX do real length later - Log.d(TAG, baf.length() + " downloaded so far"); - msg.arg1 = baf.length() % 100; - mHandler.sendMessage(msg); - } - } - String content = new String(baf.toByteArray()); - Log.d(TAG, content); - - - } catch (IOException e) { - Log.e(TAG, "Error: ",e); - } - Message msg = mHandler.obtainMessage(); - msg.arg2 = STATE_DONE; - mHandler.sendMessage(msg); - } - - public void setState(int state) { - mState = state; - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.zotero_menu, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle item selection - switch (item.getItemId()) { - case R.id.do_prefs: - startActivity(new Intent(this, SettingsActivity.class)); - return true; - default: - return super.onOptionsItemSelected(item); - } - } -} diff --git a/src/main/java/com/gimranov/zandy/app/MainActivity.java b/src/main/java/com/gimranov/zandy/app/MainActivity.java deleted file mode 100644 index 935dee0..0000000 --- a/src/main/java/com/gimranov/zandy/app/MainActivity.java +++ /dev/null @@ -1,479 +0,0 @@ -/******************************************************************************* - * This file is part of Zandy. - * - * Zandy is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Zandy is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . - ******************************************************************************/ -package com.gimranov.zandy.app; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.AdapterView; -import android.widget.Button; -import android.widget.ListView; -import android.widget.TextView; -import android.widget.Toast; - -import com.crashlytics.android.Crashlytics; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.data.Item; -import com.gimranov.zandy.app.data.ItemAdapter; -import com.gimranov.zandy.app.data.ItemCollection; -import com.gimranov.zandy.app.task.APIRequest; -import com.gimranov.zandy.app.task.ZoteroAPITask; -import com.squareup.otto.Subscribe; - -import java.util.ArrayList; - -import oauth.signpost.OAuthProvider; -import oauth.signpost.basic.DefaultOAuthProvider; -import oauth.signpost.commonshttp.CommonsHttpOAuthConsumer; -import oauth.signpost.exception.OAuthCommunicationException; -import oauth.signpost.exception.OAuthExpectationFailedException; -import oauth.signpost.exception.OAuthMessageSignerException; -import oauth.signpost.exception.OAuthNotAuthorizedException; -import oauth.signpost.http.HttpParameters; - -public class MainActivity extends Activity implements OnClickListener { - private CommonsHttpOAuthConsumer httpOAuthConsumer; - private OAuthProvider httpOAuthProvider; - - private static final String TAG = "com.gimranov.zandy.app.MainActivity"; - - private static final String DEFAULT_SORT = "timestamp ASC, item_title COLLATE NOCASE"; - - static final int DIALOG_CHOOSE_COLLECTION = 1; - - private Database db; - private Bundle b = new Bundle(); - - /** Called when the activity is first created. */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - Crashlytics.start(this); - - // Let items in on the fun - db = new Database(getBaseContext()); - - Intent intent = getIntent(); - String action = intent.getAction(); - if (action != null - && action.equals("android.intent.action.SEND") - && intent.getExtras() != null) { - // Browser sends us no data, just extras - Bundle extras = intent.getExtras(); - for (String s : extras.keySet()) { - try { - Log.d("TAG","Got extra: "+s +" => "+extras.getString(s)); - } catch (ClassCastException e) { - Log.e(TAG, "Not a string, it seems", e); - } - } - - Bundle b = new Bundle(); - b.putString("url", extras.getString("android.intent.extra.TEXT")); - b.putString("title", extras.getString("android.intent.extra.SUBJECT")); - this.b = b; - showDialog(DIALOG_CHOOSE_COLLECTION); - } - - setContentView(R.layout.main); - - Button collectionButton = (Button) findViewById(R.id.collectionButton); - collectionButton.setOnClickListener(this); - Button itemButton = (Button) findViewById(R.id.itemButton); - itemButton.setOnClickListener(this); - Button loginButton = (Button) findViewById(R.id.loginButton); - loginButton.setOnClickListener(this); - - if (ServerCredentials.check(getBaseContext())) { - setUpLoggedInUser(); - } - } - - @Override - public void onResume() { - Application.getInstance().getBus().register(this); - - Button loginButton = (Button) findViewById(R.id.loginButton); - - if (!ServerCredentials.check(getBaseContext())) { - loginButton.setText(getResources().getString(R.string.log_in)); - loginButton.setClickable(true); - } else { - refreshList(); - } - super.onResume(); - } - - @Override - protected void onPause() { - super.onPause(); - Application.getInstance().getBus().unregister(this); - } - - /** - * Refreshes the list view, safely if possible - */ - private void refreshList() { - ListView lv = ((ListView) findViewById(android.R.id.list)); - if (lv == null) return; - - ItemAdapter adapter = (ItemAdapter) lv.getAdapter(); - if (adapter != null) { - Cursor newCursor = getCursor(DEFAULT_SORT); - adapter.changeCursor(newCursor); - adapter.notifyDataSetChanged(); - } - } - - /** - * Implementation of the OnClickListener interface, to handle button events. - * - * Note: When adding a button, it needs to be added here, but the - * ClickListener needs to be set in the main onCreate(..) as well. - */ - public void onClick(View v) { - Log.d(TAG, "Click on: " + v.getId()); - if (v.getId() == R.id.collectionButton) { - Log.d(TAG, "Trying to start collection activity"); - Intent i = new Intent(this, CollectionActivity.class); - startActivity(i); - } else if (v.getId() == R.id.itemButton) { - Log.d(TAG, "Trying to start all-item activity"); - Intent i = new Intent(this, ItemActivity.class); - startActivity(i); - } else if (v.getId() == R.id.loginButton) { - Log.d(TAG, "Starting OAuth"); - new Thread(new Runnable() { - public void run() { - startOAuth(); - } - }).start(); - - } else { - Log.w(TAG, "Uncaught click on: " + v.getId()); - } - } - - /** - * Makes the OAuth call. The response on the callback is handled by the - * onNewIntent(..) method below. - * - * This will send the user to the OAuth server to get set up. - */ - protected void startOAuth() { - try { - this.httpOAuthConsumer = new CommonsHttpOAuthConsumer( - ServerCredentials.CONSUMERKEY, - ServerCredentials.CONSUMERSECRET); - this.httpOAuthProvider = new DefaultOAuthProvider( - ServerCredentials.OAUTHREQUEST, - ServerCredentials.OAUTHACCESS, - ServerCredentials.OAUTHAUTHORIZE); - - String authUrl; - authUrl = httpOAuthProvider.retrieveRequestToken(httpOAuthConsumer, - ServerCredentials.CALLBACKURL); - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(authUrl))); - } catch (OAuthMessageSignerException e) { - toastError(e.getMessage()); - } catch (OAuthNotAuthorizedException e) { - toastError(e.getMessage()); - } catch (OAuthExpectationFailedException e) { - toastError(e.getMessage()); - } catch (OAuthCommunicationException e) { - toastError(e.getMessage()); - } - } - - private void toastError(final String message) { - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG); - } - }); - } - - /** - * Receives intents that the app knows how to interpret. These will probably - * all be URIs with the protocol "zotero://". - * - * This is currently only used to receive OAuth responses, but it could be - * used with things like zotero://select and zotero://attachment in the - * future. - */ - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - Log.d(TAG, "Got new intent"); - - if (intent == null) return; - - // Here's what we do if we get a share request from the browser - String action = intent.getAction(); - if (action != null - && action.equals("android.intent.action.SEND") - && intent.getExtras() != null) { - // Browser sends us no data, just extras - Bundle extras = intent.getExtras(); - for (String s : extras.keySet()) { - try { - Log.d("TAG","Got extra: "+s +" => "+extras.getString(s)); - } catch (ClassCastException e) { - Log.e(TAG, "Not a string, it seems", e); - } - } - - Bundle b = new Bundle(); - b.putString("url", extras.getString("android.intent.extra.TEXT")); - b.putString("title", extras.getString("android.intent.extra.SUBJECT")); - this.b=b; - showDialog(DIALOG_CHOOSE_COLLECTION); - return; - } - - /* - * It's possible we've lost these to garbage collection, so we - * reinstantiate them if they turn out to be null at this point. - */ - if (this.httpOAuthConsumer == null) - this.httpOAuthConsumer = new CommonsHttpOAuthConsumer( - ServerCredentials.CONSUMERKEY, - ServerCredentials.CONSUMERSECRET); - if (this.httpOAuthProvider == null) - this.httpOAuthProvider = new DefaultOAuthProvider( - ServerCredentials.OAUTHREQUEST, - ServerCredentials.OAUTHACCESS, - ServerCredentials.OAUTHAUTHORIZE); - - /* - * Also double-check that intent isn't null, because something here - * caused a NullPointerException for a user. - */ - Uri uri; - uri = intent.getData(); - - if (uri != null) { - /* - * TODO The logic should have cases for the various things coming in - * on this protocol. - */ - final String verifier = uri - .getQueryParameter(oauth.signpost.OAuth.OAUTH_VERIFIER); - - new Thread(new Runnable() { - public void run() { - try { - /* - * Here, we're handling the callback from the completed OAuth. - * We don't need to do anything highly visible, although it - * would be nice to show a Toast or something. - */ - httpOAuthProvider.retrieveAccessToken( - httpOAuthConsumer, verifier); - HttpParameters params = httpOAuthProvider - .getResponseParameters(); - final String userID = params.getFirst("userID"); - Log.d(TAG, "uid: " + userID); - final String userKey = httpOAuthConsumer.getToken(); - Log.d(TAG, "ukey: " + userKey); - final String userSecret = httpOAuthConsumer.getTokenSecret(); - Log.d(TAG, "usecret: " + userSecret); - - runOnUiThread(new Runnable(){ - public void run(){ - /* - * These settings live in the Zotero preferences tree. - */ - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(MainActivity.this); - SharedPreferences.Editor editor = settings.edit(); - // For Zotero, the key and secret are identical, it seems - editor.putString("user_key", userKey); - editor.putString("user_secret", userSecret); - editor.putString("user_id", userID); - - editor.commit(); - - setUpLoggedInUser(); - - doSync(); - - } - }); - } catch (OAuthMessageSignerException e) { - toastError(e.getMessage()); - } catch (OAuthNotAuthorizedException e) { - toastError(e.getMessage()); - } catch (OAuthExpectationFailedException e) { - toastError(e.getMessage()); - } catch (OAuthCommunicationException e) { - toastError("Error communicating with server. Check your time settings, network connectivity, and try again. OAuth error: " + e.getMessage()); - } - } - }).start(); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.zotero_menu, menu); - - // button doesn't make sense here. - menu.removeItem(R.id.do_new); - - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle item selection - switch (item.getItemId()) { - case R.id.do_sync: - return doSync(); - case R.id.do_prefs: - startActivity(new Intent(this, SettingsActivity.class)); - return true; - case R.id.do_search: - onSearchRequested(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - private boolean doSync() { - if (!ServerCredentials.check(getApplicationContext())) { - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_log_in_first), - Toast.LENGTH_SHORT).show(); - return true; - } - Log.d(TAG, "Making sync request for all collections"); - ServerCredentials cred = new ServerCredentials(getBaseContext()); - APIRequest req = APIRequest.fetchCollections(cred); - new ZoteroAPITask(getBaseContext()).execute(req); - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_started), - Toast.LENGTH_SHORT).show(); - return true; - } - - private void setUpLoggedInUser() { - Button loginButton = (Button) findViewById(R.id.loginButton); - - loginButton.setVisibility(View.GONE); - - ItemAdapter adapter = new ItemAdapter(this, getCursor(DEFAULT_SORT)); - ListView lv = ((ListView) findViewById(android.R.id.list)); - lv.setAdapter(adapter); - - lv.setOnItemClickListener(new AdapterView.OnItemClickListener() { - public void onItemClick(AdapterView parent, View view, int position, long id) { - // If we have a click on an item, do something... - ItemAdapter adapter = (ItemAdapter) parent.getAdapter(); - Cursor cur = adapter.getCursor(); - // Place the cursor at the selected item - if (cur.moveToPosition(position)) { - // and load an activity for the item - Item item = Item.load(cur); - - Log.d(TAG, "Loading item data with key: "+item.getKey()); - // We create and issue a specified intent with the necessary data - Intent i = new Intent(getBaseContext(), ItemDataActivity.class); - i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); - i.putExtra("com.gimranov.zandy.app.itemDbId", item.dbId); - startActivity(i); - } else { - // failed to move cursor-- show a toast - TextView tvTitle = (TextView)view.findViewById(R.id.item_title); - Toast.makeText(getApplicationContext(), - getResources().getString(R.string.cant_open_item, tvTitle.getText()), - Toast.LENGTH_SHORT).show(); - } - } - }); - } - - public Cursor getCursor(String sortBy) { - Cursor cursor = db.query("items", Database.ITEMCOLS, null, null, null, null, sortBy, null); - if (cursor == null) { - Log.e(TAG, "cursor is null"); - } - return cursor; - } - - @Override - protected Dialog onCreateDialog(int id) { - final String url = b.getString("url"); - final String title = b.getString("title"); - AlertDialog dialog; - switch (id) { - case DIALOG_CHOOSE_COLLECTION: - AlertDialog.Builder builder = new AlertDialog.Builder(this); - // Now we're dealing with share link, it seems. - // For now, just add it to the main library-- we'd like to let the person choose a library, - // but not yet. - final ArrayList collections = ItemCollection.getCollections(db); - int size = collections.size(); - String[] collectionNames = new String[size]; - for (int i = 0; i < size; i++) { - collectionNames[i] = collections.get(i).getTitle(); - } - builder.setTitle(getResources().getString(R.string.choose_parent_collection)) - .setItems(collectionNames, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int pos) { - Item item = new Item(getBaseContext(), "webpage"); - item.save(db); - Log.d(TAG,"New item has key: "+item.getKey() + ", dbId: "+item.dbId); - Item.set(item.getKey(), "url", url, db); - Item.set(item.getKey(), "title", title, db); - Item.setTag(item.getKey(), null, "#added-by-zandy", 1, db); - collections.get(pos).add(item); - collections.get(pos).saveChildren(db); - Log.d(TAG, "Loading item data with key: "+item.getKey()); - // We create and issue a specified intent with the necessary data - Intent i = new Intent(getBaseContext(), ItemDataActivity.class); - i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); - i.putExtra("com.gimranov.zandy.app.itemDbId", item.dbId); - startActivity(i); - } - }); - dialog = builder.create(); - return dialog; - default: - Log.e(TAG, "Invalid dialog requested"); - return null; - } - } - - @Subscribe public void syncComplete(SyncEvent event) { - refreshList(); - } -} diff --git a/src/main/java/com/gimranov/zandy/app/Persistence.java b/src/main/java/com/gimranov/zandy/app/Persistence.java deleted file mode 100644 index 39432db..0000000 --- a/src/main/java/com/gimranov/zandy/app/Persistence.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.gimranov.zandy.app; - -import android.content.Context; -import android.content.SharedPreferences; - -import org.jetbrains.annotations.Nullable; - -public class Persistence { - private static final String TAG = Persistence.class.getCanonicalName(); - - private static final String FILE = "Persistence"; - - public static void write(String key, String value) { - SharedPreferences.Editor editor = Application.getInstance().getSharedPreferences(FILE, Context.MODE_PRIVATE).edit(); - editor.putString(key, value); - editor.commit(); - } - - @Nullable - public static String read(String key) { - SharedPreferences store = Application.getInstance().getSharedPreferences(FILE, Context.MODE_PRIVATE); - if (!store.contains(key)) return null; - - return store.getString(key, null); - } -} diff --git a/src/main/java/com/gimranov/zandy/app/SyncEvent.java b/src/main/java/com/gimranov/zandy/app/SyncEvent.java deleted file mode 100644 index 720424b..0000000 --- a/src/main/java/com/gimranov/zandy/app/SyncEvent.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.gimranov.zandy.app; - -public class SyncEvent { - private static final String TAG = SyncEvent.class.getCanonicalName(); - - public static final int COMPLETE_CODE = 1; - - public static final SyncEvent COMPLETE = new SyncEvent(COMPLETE_CODE); - - private int status; - - public SyncEvent(int status) { - this.status = status; - } - - public int getStatus() { - return status; - } -} diff --git a/src/main/java/com/gimranov/zandy/app/TagActivity.java b/src/main/java/com/gimranov/zandy/app/TagActivity.java deleted file mode 100644 index f37bbcc..0000000 --- a/src/main/java/com/gimranov/zandy/app/TagActivity.java +++ /dev/null @@ -1,264 +0,0 @@ -/******************************************************************************* - * This file is part of Zandy. - * - * Zandy is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Zandy is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . - ******************************************************************************/ -package com.gimranov.zandy.app; - -import java.util.ArrayList; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.ListActivity; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.text.Editable; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.AdapterView.OnItemLongClickListener; -import android.widget.ArrayAdapter; -import android.widget.EditText; -import android.widget.ListView; -import android.widget.TextView; -import android.widget.TextView.BufferType; -import android.widget.Toast; - -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.data.Item; -import com.gimranov.zandy.app.task.APIRequest; -import com.gimranov.zandy.app.task.ZoteroAPITask; - -/** - * This Activity handles displaying and editing tags. It works almost the same as - * ItemDataActivity, using a simple ArrayAdapter on Bundles with the tag info. - * - * @author ajlyon - * - */ -public class TagActivity extends ListActivity { - - private static final String TAG = "com.gimranov.zandy.app.TagActivity"; - - static final int DIALOG_TAG = 3; - static final int DIALOG_CONFIRM_NAVIGATE = 4; - - private Item item; - - private Database db; - - protected Bundle b = new Bundle(); - - /** Called when the activity is first created. */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - db = new Database(this); - - /* Get the incoming data from the calling activity */ - String itemKey = getIntent().getStringExtra("com.gimranov.zandy.app.itemKey"); - Item item = Item.load(itemKey, db); - this.item = item; - - this.setTitle(getResources().getString(R.string.tags_for_item, item.getTitle())); - - ArrayList rows = item.tagsToBundleArray(); - - /* - * We use the standard ArrayAdapter, passing in our data as a Bundle. - * Since it's no longer a simple TextView, we need to override getView, but - * we can do that anonymously. - */ - setListAdapter(new ArrayAdapter(this, R.layout.list_data, rows) { - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View row; - - // We are reusing views, but we need to initialize it if null - if (null == convertView) { - LayoutInflater inflater = getLayoutInflater(); - row = inflater.inflate(R.layout.list_data, null); - } else { - row = convertView; - } - - /* Our layout has just two fields */ - TextView tvLabel = (TextView) row.findViewById(R.id.data_label); - TextView tvContent = (TextView) row.findViewById(R.id.data_content); - - if (getItem(position).getInt("type") == 1) - tvLabel.setText(getResources().getString(R.string.tag_auto)); - else - tvLabel.setText(getResources().getString(R.string.tag_user)); - tvContent.setText(getItem(position).getString("tag")); - - return row; - } - }); - - ListView lv = getListView(); - lv.setTextFilterEnabled(true); - lv.setOnItemClickListener(new OnItemClickListener() { - // Warning here because Eclipse can't tell whether my ArrayAdapter is - // being used with the correct parametrization. - @SuppressWarnings("unchecked") - public void onItemClick(AdapterView parent, View view, int position, long id) { - // If we have a click on an entry, prompt to view that tag's items. - ArrayAdapter adapter = (ArrayAdapter) parent.getAdapter(); - Bundle row = adapter.getItem(position); - removeDialog(DIALOG_CONFIRM_NAVIGATE); - TagActivity.this.b = row; - showDialog(DIALOG_CONFIRM_NAVIGATE); - } - }); - - /* - * On long click, we bring up an edit dialog. - */ - lv.setOnItemLongClickListener(new OnItemLongClickListener() { - /* - * Same annotation as in onItemClick(..), above. - */ - @SuppressWarnings("unchecked") - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { - // If we have a long click on an entry, show an editor - ArrayAdapter adapter = (ArrayAdapter) parent.getAdapter(); - Bundle row = adapter.getItem(position); - - removeDialog(DIALOG_TAG); - TagActivity.this.b=row; - showDialog(DIALOG_TAG); - return true; - } - }); - - } - - @Override - public void onDestroy() { - if (db != null) db.close(); - super.onDestroy(); - } - - @Override - public void onResume() { - if (db == null) db = new Database(this); - super.onResume(); - } - - protected Dialog onCreateDialog(int id) { - @SuppressWarnings("unused") - final int type = b.getInt("type"); - final String tag = b.getString("tag"); - final String itemKey = b.getString("itemKey"); - AlertDialog dialog; - - switch (id) { - /* Simple editor for a single tag */ - case DIALOG_TAG: - final EditText input = new EditText(this); - input.setText(tag, BufferType.EDITABLE); - - dialog = new AlertDialog.Builder(this) - .setTitle(getResources().getString(R.string.tag_edit)) - .setView(input) - .setPositiveButton(getResources().getString(R.string.ok), new DialogInterface.OnClickListener() { - @SuppressWarnings("unchecked") - public void onClick(DialogInterface dialog, int whichButton) { - Editable value = input.getText(); - Log.d(TAG, "Got tag: "+value.toString()); - Item.setTag(itemKey, tag, value.toString(), 0, db); - Item item = Item.load(itemKey, db); - Log.d(TAG, "Have JSON: "+item.getContent().toString()); - ArrayAdapter la = (ArrayAdapter) getListAdapter(); - la.clear(); - for (Bundle b : item.tagsToBundleArray()) { - la.add(b); - } - la.notifyDataSetChanged(); - } - }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - // do nothing - } - }).create(); - return dialog; - case DIALOG_CONFIRM_NAVIGATE: - dialog = new AlertDialog.Builder(this) - .setTitle(getResources().getString(R.string.tag_view_confirm)) - .setPositiveButton(getResources().getString(R.string.tag_view), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - Intent i = new Intent(getBaseContext(), ItemActivity.class); - i.putExtra("com.gimranov.zandy.app.tag", tag); - startActivity(i); - } - }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - // do nothing - } - }).create(); - return dialog; - default: - Log.e(TAG, "Invalid dialog requested"); - return null; - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.zotero_menu, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle item selection - switch (item.getItemId()) { - case R.id.do_sync: - if (!ServerCredentials.check(getApplicationContext())) { - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_log_in_first), - Toast.LENGTH_SHORT).show(); - return true; - } - Log.d(TAG, "Preparing sync requests"); - new ZoteroAPITask(getBaseContext()).execute(APIRequest.update(this.item)); - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_started), - Toast.LENGTH_SHORT).show(); - return true; - case R.id.do_new: - Bundle row = new Bundle(); - row.putString("tag", ""); - row.putString("itemKey", this.item.getKey()); - row.putInt("type", 0); - removeDialog(DIALOG_TAG); - this.b = row; - showDialog(DIALOG_TAG); - return true; - case R.id.do_prefs: - startActivity(new Intent(this, SettingsActivity.class)); - return true; - default: - return super.onOptionsItemSelected(item); - } - } -} diff --git a/src/main/java/com/gimranov/zandy/app/Util.java b/src/main/java/com/gimranov/zandy/app/Util.java deleted file mode 100644 index 81346ce..0000000 --- a/src/main/java/com/gimranov/zandy/app/Util.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.gimranov.zandy.app; - -public class Util { - private static final String TAG = Util.class.getCanonicalName(); - - public static final String DOI_PREFIX = "http://dx.doi.org/"; - - public static String doiToUri(String doi) { - if (isDoi(doi)) { - return DOI_PREFIX + doi.replaceAll("^doi:", ""); - } - return doi; - } - - public static boolean isDoi(String doi) { - return (doi.startsWith("doi:") || doi.startsWith("10.")); - } -} diff --git a/src/main/java/com/gimranov/zandy/app/task/APIEvent.java b/src/main/java/com/gimranov/zandy/app/task/APIEvent.java deleted file mode 100644 index 21028dc..0000000 --- a/src/main/java/com/gimranov/zandy/app/task/APIEvent.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.gimranov.zandy.app.task; - -public interface APIEvent { - public void onComplete(APIRequest request); - - public void onUpdate(APIRequest request); - - public void onError(APIRequest request, Exception exception); - public void onError(APIRequest request, int error); -} diff --git a/src/main/java/com/mattrobertson/zotable/app/Activity_Main.java b/src/main/java/com/mattrobertson/zotable/app/Activity_Main.java new file mode 100755 index 0000000..b6ac7b8 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/Activity_Main.java @@ -0,0 +1,500 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.Bundle; + +import android.preference.PreferenceManager; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBarDrawerToggle; +import android.util.Log; +import android.view.Gravity; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import oauth.signpost.OAuthProvider; +import oauth.signpost.basic.DefaultOAuthProvider; +import oauth.signpost.commonshttp.CommonsHttpOAuthConsumer; +import oauth.signpost.exception.OAuthCommunicationException; +import oauth.signpost.exception.OAuthExpectationFailedException; +import oauth.signpost.exception.OAuthMessageSignerException; +import oauth.signpost.exception.OAuthNotAuthorizedException; +import oauth.signpost.http.HttpParameters; + +/** + * Created by Matt on 7/22/2015. + */ +public class Activity_Main extends FragmentActivity { + + private static final String TAG = "Activity_Main"; + + private CommonsHttpOAuthConsumer httpOAuthConsumer; + private OAuthProvider httpOAuthProvider; + + private DrawerLayout drawerLayout; + private ActionBarDrawerToggle drawerToggle; + + private Button btnLogin; + private LinearLayout conItems, conCollections, conTags, conFavorites; + private TextView tvItemsLabel, tvCollectionsLabel, tvTagsLabel, tvFavsLabel, tvSettingsLabel; + private ImageView imgItems, imgCollections, imgTags, imgFavs, imgSettings; + + private Fragment fragment; + + private Bundle b = new Bundle(); + + private String curTitle = "All Items"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + drawerLayout = (DrawerLayout)findViewById(R.id.drawer_layout); + + drawerToggle = new ActionBarDrawerToggle( + this, + drawerLayout, + R.string.drawer_open, + R.string.drawer_close + ) { + public void onDrawerClosed(View view) { + super.onDrawerClosed(view); + + if (getActionBar() != null) + getActionBar().setTitle(curTitle); + + invalidateOptionsMenu(); + } + + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + + if (getActionBar() != null) + getActionBar().setTitle(getResources().getString(R.string.app_name)); + + invalidateOptionsMenu(); + } + }; + + LinearLayout leftDrawerFrame = (LinearLayout)findViewById(R.id.left_drawer); + leftDrawerFrame.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // Do nothing -- intercepts touches to background listview + } + }); + + btnLogin = (Button)findViewById(R.id.btnLogin); + + btnLogin.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + new Thread(new Runnable() { + public void run() { + startOAuth(); + } + }).start(); + } + }); + + // If logged in, hide login button -- if not, open drawer for user to log in + if (ServerCredentials.check(getBaseContext())) { + btnLogin.setVisibility(View.GONE); + + RelativeLayout conSearch = (RelativeLayout)findViewById(R.id.conSearch); + conSearch.setVisibility(View.VISIBLE); + final EditText etSearch = (EditText)findViewById(R.id.etSearch); + + Button btnDoSearch = (Button)findViewById(R.id.btnDoSearch); + btnDoSearch.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent iSearch = new Intent(Activity_Main.this, SearchActivity.class); + iSearch.putExtra("query",etSearch.getText().toString()); + startActivity(iSearch); + } + }); + } + else { + drawerLayout.openDrawer(Gravity.START); + } + + conItems = (LinearLayout)findViewById(R.id.conItems); + conCollections = (LinearLayout)findViewById(R.id.conCollections); + conTags = (LinearLayout)findViewById(R.id.conTags); + conFavorites = (LinearLayout)findViewById(R.id.conFavorites); + + tvItemsLabel = (TextView)findViewById(R.id.tvItemsLabel); + tvCollectionsLabel = (TextView)findViewById(R.id.tvCollectionsLabel); + tvTagsLabel = (TextView)findViewById(R.id.tvTagsLabel); + tvFavsLabel = (TextView)findViewById(R.id.tvFavoritesLabel); + tvSettingsLabel = (TextView)findViewById(R.id.tvSettingsLabel); + + DrawerNavListener navListener = new DrawerNavListener(); + + conItems.setOnClickListener(navListener); + conCollections.setOnClickListener(navListener); + conTags.setOnClickListener(navListener); + conFavorites.setOnClickListener(navListener); + tvSettingsLabel.setOnClickListener(navListener); + + imgItems = (ImageView)findViewById(R.id.imgItemsIcon); + imgCollections = (ImageView)findViewById(R.id.imgCollectionsIcon); + imgTags = (ImageView)findViewById(R.id.imgTagsIcon); + imgFavs = (ImageView)findViewById(R.id.imgFavoritesIcon); + + fragment = new Fragment_Items(); + + // Insert the fragment by replacing any existing fragment + FragmentManager fragmentManager = getSupportFragmentManager(); + fragmentManager.beginTransaction() + .replace(R.id.content_frame, fragment) + .commit(); + + if (getActionBar() != null) { + getActionBar().setTitle(curTitle); + + getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setHomeButtonEnabled(true); + } + + // Set the drawer toggle as the DrawerListener + drawerLayout.setDrawerListener(drawerToggle); + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + // Sync the toggle state after onRestoreInstanceState has occurred. + drawerToggle.syncState(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + drawerToggle.onConfigurationChanged(newConfig); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Pass the event to ActionBarDrawerToggle, if it returns + // true, then it has handled the app icon touch event + if (drawerToggle.onOptionsItemSelected(item)) { + return true; + } + // Handle your other action bar items... + + return super.onOptionsItemSelected(item); + } + + /** + * Makes the OAuth call. The response on the callback is handled by the + * onNewIntent(..) method below. + * + * This will send the user to the OAuth server to get set up. + */ + protected void startOAuth() { + try { + this.httpOAuthConsumer = new CommonsHttpOAuthConsumer( + ServerCredentials.CONSUMERKEY, + ServerCredentials.CONSUMERSECRET); + this.httpOAuthProvider = new DefaultOAuthProvider( + ServerCredentials.OAUTHREQUEST, + ServerCredentials.OAUTHACCESS, + ServerCredentials.OAUTHAUTHORIZE); + + String authUrl; + authUrl = httpOAuthProvider.retrieveRequestToken(httpOAuthConsumer, + ServerCredentials.CALLBACKURL); + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(authUrl))); + } catch (OAuthMessageSignerException e) { + toastError(e.getMessage()); + } catch (OAuthNotAuthorizedException e) { + toastError(e.getMessage()); + } catch (OAuthExpectationFailedException e) { + toastError(e.getMessage()); + } catch (OAuthCommunicationException e) { + toastError(e.getMessage()); + } + } + + private void toastError(final String message) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(Activity_Main.this, message, Toast.LENGTH_LONG); + } + }); + } + + /** + * Receives intents that the app knows how to interpret. These will probably + * all be URIs with the protocol "zotable://". + * + * This is currently only used to receive OAuth responses, but it could be + * used with things like zotable://select and zotable://attachment in the + * future. + */ + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + Log.d(TAG, "Got new intent"); + + if (intent == null) return; + + // Here's what we do if we get a share request from the browser + String action = intent.getAction(); + if (action != null + && action.equals("android.intent.action.SEND") + && intent.getExtras() != null) { + // Browser sends us no data, just extras + Bundle extras = intent.getExtras(); + for (String s : extras.keySet()) { + try { + Log.d("TAG","Got extra: "+s +" => "+extras.getString(s)); + } catch (ClassCastException e) { + Log.e(TAG, "Not a string, it seems", e); + } + } + + Bundle b = new Bundle(); + b.putString("url", extras.getString("android.intent.extra.TEXT")); + b.putString("title", extras.getString("android.intent.extra.SUBJECT")); + + this.b=b; + + // showDialog(DIALOG_CHOOSE_COLLECTION); -- no longer needed(?) + return; + } + + /* + * It's possible we've lost these to garbage collection, so we + * reinstantiate them if they turn out to be null at this point. + */ + if (this.httpOAuthConsumer == null) + this.httpOAuthConsumer = new CommonsHttpOAuthConsumer( + ServerCredentials.CONSUMERKEY, + ServerCredentials.CONSUMERSECRET); + if (this.httpOAuthProvider == null) + this.httpOAuthProvider = new DefaultOAuthProvider( + ServerCredentials.OAUTHREQUEST, + ServerCredentials.OAUTHACCESS, + ServerCredentials.OAUTHAUTHORIZE); + + /* + * Also double-check that intent isn't null, because something here + * caused a NullPointerException for a user. + */ + Uri uri; + uri = intent.getData(); + + if (uri != null) { + /* + * TODO The logic should have cases for the various things coming in + * on this protocol. + */ + final String verifier = uri + .getQueryParameter(oauth.signpost.OAuth.OAUTH_VERIFIER); + + new Thread(new Runnable() { + public void run() { + try { + /* + * Here, we're handling the callback from the completed OAuth. + * We don't need to do anything highly visible, although it + * would be nice to show a Toast or something. + */ + httpOAuthProvider.retrieveAccessToken( + httpOAuthConsumer, verifier); + HttpParameters params = httpOAuthProvider + .getResponseParameters(); + final String userID = params.getFirst("userID"); + Log.d(TAG, "uid: " + userID); + final String userKey = httpOAuthConsumer.getToken(); + Log.d(TAG, "ukey: " + userKey); + final String userSecret = httpOAuthConsumer.getTokenSecret(); + Log.d(TAG, "usecret: " + userSecret); + + runOnUiThread(new Runnable(){ + public void run(){ + /* + * These settings live in the Zotero preferences_old tree. + */ + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(Activity_Main.this); + SharedPreferences.Editor editor = settings.edit(); + // For Zotero, the key and secret are identical, it seems + editor.putString("user_key", userKey); + editor.putString("user_secret", userSecret); + editor.putString("user_id", userID); + + editor.commit(); + + // setUpLoggedInUser(); -- no longer needed(?) + + // If logged in & Items fragment has been initialized, sync items through + // fragment's sync method + if (fragment != null) { + if (fragment.getClass() == Fragment_Items.class) { + ((Fragment_Items)fragment).sync(); + } + } + + // Close the nav drawer & hide the login button + drawerLayout.closeDrawers(); + btnLogin.setVisibility(View.GONE); + } + }); + } catch (OAuthMessageSignerException e) { + toastError(e.getMessage()); + } catch (OAuthNotAuthorizedException e) { + toastError(e.getMessage()); + } catch (OAuthExpectationFailedException e) { + toastError(e.getMessage()); + } catch (OAuthCommunicationException e) { + toastError("Error communicating with server. Check your time settings, network connectivity, and try again. OAuth error: " + e.getMessage()); + } + } + }).start(); + } + } + + private class DrawerNavListener implements View.OnClickListener { + @Override + public void onClick(View v) { + Fragment fragment = new Fragment_Items(); + + if (v.getId() == R.id.conItems) { + fragment = new Fragment_Items(); + curTitle = "All Items"; + + // Update Drawer appearance + tvItemsLabel.setTypeface(null, Typeface.BOLD); + tvItemsLabel.setTextColor(getResources().getColor(R.color.primary)); + tvCollectionsLabel.setTypeface(null, Typeface.NORMAL); + tvCollectionsLabel.setTextColor(getResources().getColor(R.color.text_color)); + tvTagsLabel.setTypeface(null, Typeface.NORMAL); + tvTagsLabel.setTextColor(getResources().getColor(R.color.text_color)); + tvFavsLabel.setTypeface(null, Typeface.NORMAL); + tvFavsLabel.setTextColor(getResources().getColor(R.color.text_color)); + + imgItems.setImageResource(R.drawable.ic_item_blue); + imgCollections.setImageResource(R.drawable.ic_collection_black); + imgTags.setImageResource(R.drawable.ic_tag_black); + imgFavs.setImageResource(R.drawable.ic_favorites_black); + + FragmentManager fragmentManager = getSupportFragmentManager(); + fragmentManager.beginTransaction() + .replace(R.id.content_frame, fragment) + .commit(); + } + else if (v.getId() == R.id.conCollections) { + fragment = new Fragment_Collections(); + curTitle = "Collections"; + + // Update Drawer appearance + tvItemsLabel.setTypeface(null, Typeface.NORMAL); + tvItemsLabel.setTextColor(getResources().getColor(R.color.text_color)); + tvCollectionsLabel.setTypeface(null, Typeface.BOLD); + tvCollectionsLabel.setTextColor(getResources().getColor(R.color.primary)); + tvTagsLabel.setTypeface(null, Typeface.NORMAL); + tvTagsLabel.setTextColor(getResources().getColor(R.color.text_color)); + tvFavsLabel.setTypeface(null, Typeface.NORMAL); + tvFavsLabel.setTextColor(getResources().getColor(R.color.text_color)); + + imgItems.setImageResource(R.drawable.ic_item_black); + imgCollections.setImageResource(R.drawable.ic_collection_blue); + imgTags.setImageResource(R.drawable.ic_tag_black); + imgFavs.setImageResource(R.drawable.ic_favorites_black); + + FragmentManager fragmentManager = getSupportFragmentManager(); + fragmentManager.beginTransaction() + .replace(R.id.content_frame, fragment) + .commit(); + } + else if (v.getId() == R.id.conTags) { + fragment = new Fragment_Tags(); + curTitle = "Tags"; + + // Update Drawer appearance + tvItemsLabel.setTypeface(null, Typeface.NORMAL); + tvItemsLabel.setTextColor(getResources().getColor(R.color.text_color)); + tvCollectionsLabel.setTypeface(null, Typeface.NORMAL); + tvCollectionsLabel.setTextColor(getResources().getColor(R.color.text_color)); + tvTagsLabel.setTypeface(null, Typeface.BOLD); + tvTagsLabel.setTextColor(getResources().getColor(R.color.primary)); + tvFavsLabel.setTypeface(null, Typeface.NORMAL); + tvFavsLabel.setTextColor(getResources().getColor(R.color.text_color)); + + imgItems.setImageResource(R.drawable.ic_item_black); + imgCollections.setImageResource(R.drawable.ic_collection_black); + imgTags.setImageResource(R.drawable.ic_tag_blue); + imgFavs.setImageResource(R.drawable.ic_favorites_black); + + FragmentManager fragmentManager = getSupportFragmentManager(); + fragmentManager.beginTransaction() + .replace(R.id.content_frame, fragment) + .commit(); + } + else if (v.getId() == R.id.conFavorites) { + fragment = new Fragment_Favorites(); + curTitle = "Favorites"; + + // Update Drawer appearance + tvItemsLabel.setTypeface(null, Typeface.NORMAL); + tvItemsLabel.setTextColor(getResources().getColor(R.color.text_color)); + tvCollectionsLabel.setTypeface(null, Typeface.NORMAL); + tvCollectionsLabel.setTextColor(getResources().getColor(R.color.text_color)); + tvTagsLabel.setTypeface(null, Typeface.NORMAL); + tvTagsLabel.setTextColor(getResources().getColor(R.color.text_color)); + tvFavsLabel.setTypeface(null, Typeface.BOLD); + tvFavsLabel.setTextColor(getResources().getColor(R.color.primary)); + + imgItems.setImageResource(R.drawable.ic_item_black); + imgCollections.setImageResource(R.drawable.ic_collection_black); + imgTags.setImageResource(R.drawable.ic_tag_black); + imgFavs.setImageResource(R.drawable.ic_favorites_blue); + + FragmentManager fragmentManager = getSupportFragmentManager(); + fragmentManager.beginTransaction() + .replace(R.id.content_frame, fragment) + .commit(); + } + else if (v.getId() == R.id.tvSettingsLabel) { + Intent i = new Intent(getBaseContext(), Activity_Preference.class); + startActivity(i); + } + + drawerLayout.closeDrawers(); + } + } +} diff --git a/src/main/java/com/mattrobertson/zotable/app/Activity_Preference.java b/src/main/java/com/mattrobertson/zotable/app/Activity_Preference.java new file mode 100755 index 0000000..ed5f7d7 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/Activity_Preference.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.database.Cursor; +import android.os.Bundle; +import android.preference.PreferenceFragment; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.ItemCollection; +import com.mattrobertson.zotable.app.data.TagAdapter; + +public class Activity_Preference extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getActionBar().setDisplayHomeAsUpEnabled(true); + + // Display the fragment as the main content. + getFragmentManager().beginTransaction() + .replace(android.R.id.content, new ZotPrefFragment()) + .commit(); + } + + public static class ZotPrefFragment extends PreferenceFragment { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.settings); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + LinearLayout v = (LinearLayout) super.onCreateView(inflater, container, savedInstanceState); + + Button btnClearDb = new Button(getActivity().getApplicationContext()); + btnClearDb.setText("Clear local database"); + v.addView(btnClearDb); + + btnClearDb.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + + AlertDialog.Builder builder = new AlertDialog.Builder(new ContextThemeWrapper(getActivity(),R.style.AppTheme)); + builder.setTitle(getResources().getString(R.string.settings_reset_database_warning)) + .setPositiveButton(getResources().getString(R.string.menu_delete), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + Database db = new Database(getActivity().getBaseContext()); + db.resetAllData(); + getActivity().finish(); + } + }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // do nothing + } + }); + AlertDialog dialog = builder.show(); + + int textViewId = dialog.getContext().getResources().getIdentifier("android:id/alertTitle", null, null); + TextView tv = (TextView) dialog.findViewById(textViewId); + tv.setTextColor(getResources().getColor(R.color.white)); + } + }); + + return v; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/gimranov/zandy/app/AmazonZxingGlue.java b/src/main/java/com/mattrobertson/zotable/app/AmazonZxingGlue.java old mode 100644 new mode 100755 similarity index 64% rename from src/main/java/com/gimranov/zandy/app/AmazonZxingGlue.java rename to src/main/java/com/mattrobertson/zotable/app/AmazonZxingGlue.java index 19fdf60..bcc833b --- a/src/main/java/com/gimranov/zandy/app/AmazonZxingGlue.java +++ b/src/main/java/com/mattrobertson/zotable/app/AmazonZxingGlue.java @@ -1,4 +1,20 @@ -package com.gimranov.zandy.app; +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; import android.app.AlertDialog; import android.content.ActivityNotFoundException; diff --git a/src/main/java/com/mattrobertson/zotable/app/Application.java b/src/main/java/com/mattrobertson/zotable/app/Application.java new file mode 100755 index 0000000..ffac725 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/Application.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import com.squareup.otto.Bus; + +public class Application extends android.app.Application { + private static final String TAG = Application.class.getCanonicalName(); + + private static Application instance; + + private Bus bus; + + @Override + public void onCreate() { + super.onCreate(); + + bus = new Bus(); + + instance = this; + } + + public Bus getBus() { + return bus; + } + + public static Application getInstance() { + return instance; + } +} diff --git a/src/main/java/com/gimranov/zandy/app/AttachmentActivity.java b/src/main/java/com/mattrobertson/zotable/app/AttachmentActivity.java old mode 100644 new mode 100755 similarity index 93% rename from src/main/java/com/gimranov/zandy/app/AttachmentActivity.java rename to src/main/java/com/mattrobertson/zotable/app/AttachmentActivity.java index 371b030..4aea8ed --- a/src/main/java/com/gimranov/zandy/app/AttachmentActivity.java +++ b/src/main/java/com/mattrobertson/zotable/app/AttachmentActivity.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Zandy is distributed in the hope that it will be useful, + * Zotable is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app; +package com.mattrobertson.zotable.app; import android.app.AlertDialog; import android.app.Dialog; @@ -50,13 +50,11 @@ import android.widget.TextView.BufferType; import android.widget.Toast; -import com.crashlytics.android.Crashlytics; -import com.gimranov.zandy.app.data.Attachment; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.data.Item; -import com.gimranov.zandy.app.task.APIRequest; -import com.gimranov.zandy.app.task.ZoteroAPITask; -import com.gimranov.zandy.app.webdav.WebDavTrust; +import com.mattrobertson.zotable.app.data.Attachment; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.task.APIRequest; +import com.mattrobertson.zotable.app.webdav.WebDavTrust; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; @@ -89,7 +87,7 @@ */ public class AttachmentActivity extends ListActivity { - private static final String TAG = "com.gimranov.zandy.app.AttachmentActivity"; + private static final String TAG = "AttachmentActivity"; static final int DIALOG_CONFIRM_NAVIGATE = 4; static final int DIALOG_FILE_PROGRESS = 6; @@ -119,7 +117,7 @@ public void onCreate(Bundle savedInstanceState) { db = new Database(this); /* Get the incoming data from the calling activity */ - final String itemKey = getIntent().getStringExtra("com.gimranov.zandy.app.itemKey"); + final String itemKey = getIntent().getStringExtra("com.mattrobertson.zotable.app.itemKey"); Item item = Item.load(itemKey, db); this.item = item; @@ -199,7 +197,7 @@ public boolean onItemLongClick(AdapterView parent, View view, int position, l if (row.content.has("note")) { Log.d(TAG, "Trying to start note view activity for: " + row.key); Intent i = new Intent(getBaseContext(), NoteActivity.class); - i.putExtra("com.gimranov.zandy.app.attKey", row.key);//row.content.optString("note", "")); + i.putExtra("com.mattrobertson.zotable.app.attKey", row.key);//row.content.optString("note", "")); startActivity(i); } return true; @@ -631,7 +629,7 @@ protected void afterWrite(int n) throws IOException { ServerCredentials.sCacheDir.mkdirs(); } - File tmpFile = File.createTempFile("zandy", ".zip", ServerCredentials.sCacheDir); + File tmpFile = File.createTempFile("zotable", ".zip", ServerCredentials.sCacheDir); FileUtils.copyFile(file, tmpFile); //noinspection ResultOfMethodCallIgnored @@ -688,9 +686,7 @@ protected void afterWrite(int n) throws IOException { Log.d(TAG, "Skipping file: " + name); } } catch (IllegalArgumentException e) { - Crashlytics.logException(new Throwable("b64 " + name64, e)); } catch (NegativeArraySizeException e) { - Crashlytics.logException(new Throwable("b64 " + name64, e)); } } while (entries.hasMoreElements()); @@ -708,7 +704,6 @@ protected void afterWrite(int n) throws IOException { + " sec"); } catch (IOException e) { Log.e(TAG, "Error: ", e); - Crashlytics.logException(e); toastError(R.string.attachment_download_failed, e.getMessage()); } @@ -755,24 +750,6 @@ public boolean onOptionsItemSelected(MenuItem item) { Bundle b = new Bundle(); // Handle item selection switch (item.getItemId()) { - case R.id.do_sync: - if (!ServerCredentials.check(getApplicationContext())) { - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_log_in_first), - Toast.LENGTH_SHORT).show(); - return true; - } - Log.d(TAG, "Preparing sync requests, starting with present item"); - new ZoteroAPITask(getBaseContext()).execute(APIRequest.update(this.item)); - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_started), - Toast.LENGTH_SHORT).show(); - - return true; - case R.id.do_new: - b.putString("itemKey", this.item.getKey()); - b.putString("mode", "new"); - removeDialog(DIALOG_NOTE); - showDialog(DIALOG_NOTE); - return true; case R.id.do_prefs: startActivity(new Intent(this, SettingsActivity.class)); return true; diff --git a/src/main/java/com/mattrobertson/zotable/app/CollectionActivity.java b/src/main/java/com/mattrobertson/zotable/app/CollectionActivity.java new file mode 100755 index 0000000..64fc5b1 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/CollectionActivity.java @@ -0,0 +1,223 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.app.Activity; +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import com.mattrobertson.zotable.app.data.CollectionAdapter; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.ItemCollection; +import com.mattrobertson.zotable.app.task.APIRequest; + +public class CollectionActivity extends Activity { + + private static final String TAG = "CollectionActivity"; + private ItemCollection collection; + private Database db; + + ListView lvCollections; + + final Handler handler = new Handler() { + public void handleMessage(Message msg) { + Log.d(TAG,"received message: "+msg.arg1); + refreshView(); + + if (msg.arg1 == APIRequest.UPDATED_DATA) { + //refreshView(); + return; + } + + if (msg.arg1 == APIRequest.QUEUED_MORE) { + Toast.makeText(getApplicationContext(), + getResources().getString(R.string.sync_queued_more, msg.arg2), + Toast.LENGTH_SHORT).show(); + return; + } + + if (msg.arg1 == APIRequest.BATCH_DONE) { + Application.getInstance().getBus().post(SyncEvent.COMPLETE); + //Toast.makeText(getApplicationContext(),getResources().getString(R.string.sync_complete),Toast.LENGTH_SHORT).show(); + return; + } + + if (msg.arg1 == APIRequest.ERROR_UNKNOWN) { + String desc = (msg.arg2 == 0) ? "" : " ("+msg.arg2+")"; + Toast.makeText(getApplicationContext(), + getResources().getString(R.string.sync_error)+desc, + Toast.LENGTH_SHORT).show(); + return; + } + } + }; + + /** + * Refreshes the current list adapter + */ + private void refreshView() { + CollectionAdapter adapter = (CollectionAdapter) (lvCollections.getAdapter()); + Cursor newCursor = (collection == null) ? create() : create(collection); + adapter.changeCursor(newCursor); + adapter.notifyDataSetChanged(); + Log.d(TAG, "refreshing view on request"); + } + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getActionBar().setDisplayHomeAsUpEnabled(true); + + db = new Database(this); + + setContentView(R.layout.collections); + + lvCollections = (ListView)findViewById(R.id.lvCollections); + + CollectionAdapter collectionAdapter; + + String collectionKey = getIntent().getStringExtra("com.mattrobertson.zotable.app.collectionKey"); + if (collectionKey != null) { + ItemCollection coll = ItemCollection.load(collectionKey, db); + // We set the title to the current collection + this.collection = coll; + this.setTitle(coll.getTitle()); + collectionAdapter = new CollectionAdapter(this, create(coll)); + } else { + this.setTitle(getResources().getString(R.string.collections)); + collectionAdapter = new CollectionAdapter(this, create()); + } + + lvCollections.setAdapter(collectionAdapter); + + lvCollections.setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView parent, View view, int position, long id) { + + CollectionAdapter adapter = (CollectionAdapter) parent.getAdapter(); + Cursor cur = adapter.getCursor(); + // Place the cursor at the selected item + if (cur.moveToPosition(position)) { + // and replace the cursor with one for the selected collection + ItemCollection coll = ItemCollection.load(cur); + if (coll != null && coll.getKey() != null) { + Intent i = new Intent(getBaseContext(), ItemActivity.class); + if (coll.getSize() == 0) { + // Send a message that we need to refresh the collection + i.putExtra("com.mattrobertson.zotable.app.rerequest", true); + } + i.putExtra("com.mattrobertson.zotable.app.collectionKey", coll.getKey()); + startActivity(i); + } else { + // collection loaded was null. why? + Log.d(TAG, "Failed loading items for collection at position: " + position); + return; + } + } else { + // failed to move cursor-- show a toast + TextView tvTitle = (TextView) view.findViewById(R.id.collection_title); + Toast.makeText(getApplicationContext(), + getResources().getString(R.string.collection_cant_open, tvTitle.getText()), + Toast.LENGTH_SHORT).show(); + return; + } + return; + } + }); + } + + protected void onResume() { + CollectionAdapter adapter = (CollectionAdapter)(lvCollections.getAdapter()); + // XXX This may be too agressive-- fix if causes issues + Cursor newCursor = (collection == null) ? create() : create(collection); + adapter.changeCursor(newCursor); + adapter.notifyDataSetChanged(); + if (db == null) db = new Database(this); + super.onResume(); + } + + public void onDestroy() { + CollectionAdapter adapter = (CollectionAdapter)(lvCollections.getAdapter()); + Cursor cur = adapter.getCursor(); + if(cur != null) cur.close(); + if (db != null) db.close(); + super.onDestroy(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.zotero_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + switch (item.getItemId()) { + case android.R.id.home: + onBackPressed(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + /** + * Gives a cursor for top-level collections + * @return + */ + public Cursor create() { + String[] args = { "false" }; + Cursor cursor = db.query("collections", Database.COLLCOLS, "collection_parent=?", args, null, null, "collection_name", null); + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + + return cursor; + } + + /** + * Gives a cursor for child collections of a given parent + * @param parent + * @return + */ + public Cursor create(ItemCollection parent) { + String[] args = { parent.getKey() }; + Cursor cursor = db.query("collections", Database.COLLCOLS, "collection_parent=?", args, null, null, "collection_name", null); + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + + return cursor; + } + +} diff --git a/src/main/java/com/mattrobertson/zotable/app/CollectionMembershipActivity.java b/src/main/java/com/mattrobertson/zotable/app/CollectionMembershipActivity.java new file mode 100755 index 0000000..c5ce361 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/CollectionMembershipActivity.java @@ -0,0 +1,304 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import java.util.ArrayList; +import java.util.List; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ListActivity; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + + +import com.getbase.floatingactionbutton.FloatingActionButton; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemCollection; + +/** + * This Activity handles displaying and editing collection memberships for a + * given item. + * + * @author ajlyon + * + */ +public class CollectionMembershipActivity extends Activity { + + private static final String TAG = "CollMembershipActivity"; + + private String itemKey; + private String itemTitle; + private Item item; + + ListView lvCollections; + ArrayList rows; + CollectionMembershipAdapter adapter; + + private Database db; + + /** + * For API <= 7, where we can't pass Bundles to dialogs + */ + private Bundle b = new Bundle(); + + ArrayList arrCollKeys,arrCollNames; + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.coll_mem_activity); + + getActionBar().setDisplayHomeAsUpEnabled(true); + + db = new Database(this); + + /* Get the incoming data from the calling activity */ + itemKey = getIntent().getStringExtra("com.mattrobertson.zotable.app.itemKey"); + item = Item.load(itemKey, db); + if (item == null) { + Log.e(TAG, "Null item for key: "+itemKey); + finish(); + } + itemTitle = item.getTitle(); + + this.setTitle(getResources().getString(R.string.collections_for_item, itemTitle)); + + lvCollections = (ListView)findViewById(R.id.lvCollections); + + rows = ItemCollection.getCollections(item, db); + + adapter = new CollectionMembershipAdapter(rows); + + lvCollections.setAdapter(adapter); + + // Fill Collection arrays - used to add to collection. Pre-pop for better performance. + new Thread(new Runnable() { + public void run() { + fillCollsArr(); + } + }).start(); + + FloatingActionButton fab = (FloatingActionButton)findViewById(R.id.btnFab); + + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + + AlertDialog.Builder builder = new AlertDialog.Builder(CollectionMembershipActivity.this); + builder.setTitle("Add to collection"); + + ListView lvColls = new ListView(CollectionMembershipActivity.this); + + String[] templ = {}; + String[] stringArray = arrCollNames.toArray(templ); + + ArrayAdapter modeAdapter = new ArrayAdapter<>(CollectionMembershipActivity.this, android.R.layout.simple_list_item_1, android.R.id.text1, stringArray); + lvColls.setAdapter(modeAdapter); + + builder.setView(lvColls); + final Dialog dialog = builder.create(); + + lvColls.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + //Toast.makeText(getBaseContext(),"Clicked "+arrCollNames.get(position).trim()+" ("+arrCollKeys.get(position)+")",Toast.LENGTH_LONG).show(); + + ItemCollection coll = ItemCollection.load(arrCollKeys.get(position), db); + coll.add(item, false, db); + coll.saveChildren(db); + + refreshData(); + + dialog.dismiss(); + } + }); + + dialog.show(); + } + }); + } + + @Override + public void onDestroy() { + if (db != null) db.close(); + super.onDestroy(); + } + + @Override + public void onResume() { + if (db == null) db = new Database(this); + super.onResume(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + switch (item.getItemId()) { + case android.R.id.home: + onBackPressed(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + private void refreshData() { + rows = ItemCollection.getCollections(item, db); + + adapter.setData(rows); + + adapter.notifyDataSetChanged(); + lvCollections.invalidate(); + } + + public void fillCollsArr() { + arrCollKeys = new ArrayList<>(); + arrCollNames = new ArrayList<>(); + + String[] args = { "false" }; + Cursor rootCursor = db.query("collections", Database.COLLCOLS, "collection_parent=?", args, null, null, "collection_name", null); + + if (rootCursor == null) + return; + + rootCursor.moveToPrevious(); + + while (rootCursor.moveToNext()) { + String collName = rootCursor.getString(rootCursor.getColumnIndex("collection_name")); + String collKey = rootCursor.getString(rootCursor.getColumnIndex("collection_key")); + + arrCollKeys.add(collKey); + arrCollNames.add(collName); + + getChildren(collKey, 1); + } + + rootCursor.close(); + } + + private void getChildren(String parentKey, int level) { + String[] args = { parentKey }; + Cursor childCursor = db.query("collections", Database.COLLCOLS, "collection_parent=?", args, null, null, "collection_name", null); + + if (childCursor == null) + return; + + childCursor.moveToPrevious(); + + while (childCursor.moveToNext()) { + String collName = childCursor.getString(childCursor.getColumnIndex("collection_name")); + String collKey = childCursor.getString(childCursor.getColumnIndex("collection_key")); + + arrCollKeys.add(collKey); + + // Period in front of name designates a level of depth in hierarchy + for (int i=0; i mList; + + public CollectionMembershipAdapter(ArrayList list) { + mList = list; + } + + public void setData(ArrayList newColl) { + mList = newColl; + } + + @Override + public int getCount() { + return mList.size(); + } + @Override + public Object getItem(int pos) { + return mList.get(pos); + } + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View row; + + // We are reusing views, but we need to initialize it if null + if (null == convertView) { + LayoutInflater inflater = getLayoutInflater(); + row = inflater.inflate(R.layout.list_coll_mem, null); + } else { + row = convertView; + } + + final String collKey = ((ItemCollection)getItem(position)).getKey(); + final String title = ((ItemCollection)getItem(position)).getTitle(); + + TextView tvName = (TextView) row.findViewById(R.id.tvCollName); + tvName.setText(title); + + ImageView imgRemove = (ImageView) row.findViewById(R.id.imgRemove); + imgRemove.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + CollectionMembershipActivity.this.runOnUiThread(new Runnable() { + public void run() { + ItemCollection col = ItemCollection.load(collKey,db); + col.remove(item,false,db); + + refreshData(); + } + }); + } + }); + + return row; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mattrobertson/zotable/app/CreatorActivity.java b/src/main/java/com/mattrobertson/zotable/app/CreatorActivity.java new file mode 100755 index 0000000..f9aca61 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/CreatorActivity.java @@ -0,0 +1,369 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import java.util.ArrayList; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ListActivity; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.ArrayAdapter; +import android.widget.BaseAdapter; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + + +import com.getbase.floatingactionbutton.FloatingActionButton; +import com.mattrobertson.zotable.app.data.Creator; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemCollection; + +public class CreatorActivity extends Activity { + + private static final String TAG = "CreatorActivity"; + + static final int DIALOG_CREATOR = 3; + + ListView lvCreators; + CreatorAdapter adapter; + + public Item item; + + private Database db; + + /** + * For API <= 7, to pass bundles to activities + */ + private Bundle b = new Bundle(); + + /** + * Called when the activity is first created. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.creator_activity); + + db = new Database(this); + + lvCreators = (ListView) findViewById(R.id.lvCreators); + + /* Get the incoming data from the calling activity */ + String itemKey = getIntent().getStringExtra("com.mattrobertson.zotable.app.itemKey"); + Item item = Item.load(itemKey, db); + this.item = item; + + this.setTitle("Creators for " + item.getTitle()); + + ArrayList rows = item.creatorsToBundleArray(); + adapter = new CreatorAdapter(rows); + lvCreators.setAdapter(adapter); + + lvCreators.setTextFilterEnabled(true); + lvCreators.setOnItemClickListener(new OnItemClickListener() { + // Warning here because Eclipse can't tell whether my ArrayAdapter is + // being used with the correct parametrization. + @SuppressWarnings("unchecked") + public void onItemClick(AdapterView parent, View view, int position, long id) { + // If we have a click on an entry, do something... + CreatorAdapter adapter = (CreatorAdapter) parent.getAdapter(); + Bundle row = (Bundle) (adapter.getItem(position)); + + CreatorActivity.this.b = row; + removeDialog(DIALOG_CREATOR); + showDialog(DIALOG_CREATOR); + } + }); + + FloatingActionButton fab = (FloatingActionButton)findViewById(R.id.btnFab); + + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + //Toast.makeText(CreatorActivity.this,"Adding",Toast.LENGTH_SHORT).show(); + + // Set local Bundle b to null to show we are adding a new Creator + CreatorActivity.this.b = null; + removeDialog(DIALOG_CREATOR); + showDialog(DIALOG_CREATOR); + } + }); + } + + @Override + public void onDestroy() { + if (db != null) db.close(); + super.onDestroy(); + } + + @Override + public void onResume() { + if (db == null) db = new Database(this); + super.onResume(); + } + + public void refreshData() { + ArrayList rows = item.creatorsToBundleArray(); + adapter = new CreatorAdapter(rows); + lvCreators.setAdapter(adapter); + + adapter.notifyDataSetChanged(); + lvCreators.invalidate(); + } + + protected Dialog onCreateDialog(int id) { + + final String creatorType; + final int creatorPosition; + + String name, firstName, lastName; + + if (b != null) { + creatorType = b.getString("creatorType"); + creatorPosition = b.getInt("position"); + name = b.getString("name"); + firstName = b.getString("firstName"); + lastName = b.getString("lastName"); + } + else { + creatorType = ""; + creatorPosition = -1; + firstName = "First"; + lastName = "Last"; + name = firstName + " " + lastName; + } + + switch (id) { + /* Editor for a creator + */ + case DIALOG_CREATOR: + AlertDialog.Builder builder; + AlertDialog dialog; + + LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); + final View layout = inflater.inflate(R.layout.creator_dialog, (ViewGroup) findViewById(R.id.layout_root)); + + TextView textName = (TextView) layout.findViewById(R.id.creator_name); + TextView textFN = (TextView) layout.findViewById(R.id.creator_firstName); + TextView textLN = (TextView) layout.findViewById(R.id.creator_lastName); + + textName.setText(name); + textFN.setText(firstName); + textLN.setText(lastName); + + CheckBox mode = (CheckBox) layout.findViewById(R.id.creator_mode); + mode.setChecked((firstName == null || firstName.equals("")) + && (lastName == null || lastName.equals("")) + && (lastName != null && !name.equals(""))); + + // Set up the adapter to get creator types + String[] types = Item.localizedCreatorTypesForItemType(item.getType()); + + // what position are we? + int arrPosition = 0; + String localType = ""; + + if (creatorType != null) { + localType = Item.localizedStringForString(creatorType); + } + else { // We default to the first possibility when none specified + localType = Item.localizedStringForString(Item.creatorTypesForItemType(item.getType())[0]); + } + + // Set spinner to display item type + for (int i = 0; i < types.length; i++) { + if (types[i].equals(localType)) { + arrPosition = i; + break; + } + } + + ArrayAdapter adapter = new ArrayAdapter(this,android.R.layout.simple_spinner_item, types); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + + Spinner spinner = (Spinner) layout.findViewById(R.id.creator_type); + spinner.setAdapter(adapter); + + spinner.setSelection(arrPosition); + builder = new AlertDialog.Builder(this); + builder.setView(layout); + builder.setPositiveButton(getResources().getString(R.string.ok), new OnClickListener() { + @SuppressWarnings("unchecked") + public void onClick(DialogInterface dialog, int whichButton) { + Creator c; + TextView textName = (TextView) layout.findViewById(R.id.creator_name); + TextView textFN = (TextView) layout.findViewById(R.id.creator_firstName); + TextView textLN = (TextView) layout.findViewById(R.id.creator_lastName); + Spinner spinner = (Spinner) layout.findViewById(R.id.creator_type); + CheckBox mode = (CheckBox) layout.findViewById(R.id.creator_mode); + + String selected = (String) spinner.getSelectedItem(); + // Set up the adapter to get creator types + String[] types = Item.localizedCreatorTypesForItemType(item.getType()); + + // what position are we? + int typePos = 0; + for (int i = 0; i < types.length; i++) { + if (types[i].equals(selected)) { + typePos = i; + break; + } + } + + String realType = Item.creatorTypesForItemType(item.getType())[typePos]; + + if (mode.isChecked()) + c = new Creator(realType, textName.getText().toString(), true); + else + c = new Creator(realType, textFN.getText().toString(), textLN.getText().toString()); + + Item.setCreator(item.getKey(), c, creatorPosition, db); + item = Item.load(item.getKey(), db); + + refreshData(); + } + }); + + builder.setNeutralButton(getResources().getString(R.string.cancel), new OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // do nothing + } + }); + + builder.setNegativeButton(getResources().getString(R.string.menu_delete), new OnClickListener() { + @SuppressWarnings("unchecked") + public void onClick(DialogInterface dialog, int whichButton) { + Item.setCreator(item.getKey(), null, creatorPosition, db); + item = Item.load(item.getKey(), db); + + refreshData(); + } + }); + + dialog = builder.create(); + + WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); + lp.copyFrom(dialog.getWindow().getAttributes()); + lp.width = WindowManager.LayoutParams.MATCH_PARENT; + lp.height = WindowManager.LayoutParams.MATCH_PARENT; + + dialog.getWindow().setAttributes(lp); + + return dialog; + + default: + Log.e(TAG, "Invalid dialog requested"); + return null; + } + } + + /* + * I've been just copying-and-pasting the options menu code from activity to activity. + * It needs to be reworked for some of these activities. + */ + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.zotero_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + switch (item.getItemId()) { + case R.id.do_prefs: + startActivity(new Intent(this, SettingsActivity.class)); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + class CreatorAdapter extends BaseAdapter { + private ArrayList mList; + + public CreatorAdapter(ArrayList list) { + mList = list; + } + + public void setData(ArrayList newList) { + mList = newList; + } + + @Override + public int getCount() { + return mList.size(); + } + + @Override + public Object getItem(int pos) { + return mList.get(pos); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View row; + + // We are reusing views, but we need to initialize it if null + if (null == convertView) { + LayoutInflater inflater = getLayoutInflater(); + row = inflater.inflate(R.layout.list_data, null); + } else { + row = convertView; + } + + TextView tvLabel = (TextView) row.findViewById(R.id.data_label); + TextView tvContent = (TextView) row.findViewById(R.id.data_content); + ImageView imgAction = (ImageView)row.findViewById(R.id.imgAction); + + tvLabel.setText(Item.localizedStringForString(((Bundle)getItem(position)).getString("creatorType"))); + tvContent.setText(((Bundle)getItem(position)).getString("name")); + imgAction.setImageResource(R.drawable.ic_edit); + imgAction.setVisibility(View.VISIBLE); + + return row; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mattrobertson/zotable/app/Fragment_Collections.java b/src/main/java/com/mattrobertson/zotable/app/Fragment_Collections.java new file mode 100755 index 0000000..063f1b2 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/Fragment_Collections.java @@ -0,0 +1,707 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Message; +import android.support.v4.app.Fragment; +import android.support.v4.app.ListFragment; +import android.support.v4.widget.SwipeRefreshLayout; +import android.text.Editable; +import android.text.InputType; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import com.getbase.floatingactionbutton.FloatingActionButton; +import com.google.zxing.integration.android.IntentIntegrator; +import com.google.zxing.integration.android.IntentResult; +import com.mattrobertson.zotable.app.data.CollectionAdapter; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemCollection; +import com.mattrobertson.zotable.app.task.APIEvent; +import com.mattrobertson.zotable.app.task.APIRequest; +import com.mattrobertson.zotable.app.task.ZoteroAPITask; +import com.nononsenseapps.filepicker.FilePickerActivity; + +import org.apache.http.util.ByteArrayBuffer; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; + +public class Fragment_Collections extends Fragment implements SwipeRefreshLayout.OnRefreshListener { + + private static final String TAG = "Fragment_Collections"; + + static final int DIALOG_NEW = 1; + static final int DIALOG_IDENTIFIER = 3; + static final int DIALOG_PROGRESS = 6; + + static final int FILE_CODE = 21; + + Bundle b = new Bundle(); + + ListView lvCollections; + + private ProgressDialog mProgressDialog; + private ProgressThread progressThread; + + private ItemCollection collection; + private Database db; + + private SwipeRefreshLayout swipeLayout; + + final Handler syncHandler = new Handler() { + public void handleMessage(Message msg) { + Log.d(TAG,"received message: "+msg.arg1); + refreshView(); + + if (msg.arg1 == APIRequest.UPDATED_DATA) { + //refreshView(); + return; + } + + if (msg.arg1 == APIRequest.QUEUED_MORE) { + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.sync_queued_more, msg.arg2), + Toast.LENGTH_SHORT).show(); + return; + } + + if (msg.arg1 == APIRequest.BATCH_DONE) { + Application.getInstance().getBus().post(SyncEvent.COMPLETE); + //Toast.makeText(getActivity().getApplicationContext(),getResources().getString(R.string.sync_complete),Toast.LENGTH_SHORT).show(); + return; + } + + if (msg.arg1 == APIRequest.ERROR_UNKNOWN) { + String desc = (msg.arg2 == 0) ? "" : " ("+msg.arg2+")"; + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.sync_error)+desc, + Toast.LENGTH_SHORT).show(); + } + } + }; + + private APIEvent mEvent = new APIEvent() { + private int updates = 0; + + @Override + public void onComplete(APIRequest request) { + Message msg = syncHandler.obtainMessage(); + msg.arg1 = APIRequest.BATCH_DONE; + syncHandler.sendMessage(msg); + Log.d(TAG, "fired oncomplete"); + + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + // Stop the Spinning refreshing icon + if (swipeLayout != null && swipeLayout.isRefreshing()) { + swipeLayout.setRefreshing(false); + } + } + }); + } + + @Override + public void onUpdate(APIRequest request) { + updates++; + + if (updates % 10 == 0) { + Message msg = syncHandler.obtainMessage(); + msg.arg1 = APIRequest.UPDATED_DATA; + syncHandler.sendMessage(msg); + } + } + + @Override + public void onError(APIRequest request, Exception exception) { + Log.e(TAG, "APIException caught", exception); + Message msg = syncHandler.obtainMessage(); + msg.arg1 = APIRequest.ERROR_UNKNOWN; + syncHandler.sendMessage(msg); + } + + @Override + public void onError(APIRequest request, int error) { + Log.e(TAG, "API error caught"); + Message msg = syncHandler.obtainMessage(); + msg.arg1 = APIRequest.ERROR_UNKNOWN; + syncHandler.sendMessage(msg); + } + }; + + /** + * Refreshes the current list adapter + */ + private void refreshView() { + CollectionAdapter adapter = (CollectionAdapter) (lvCollections.getAdapter()); + Cursor newCursor = create(); + adapter.changeCursor(newCursor); + adapter.notifyDataSetChanged(); + Log.d(TAG, "refreshing view on request"); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_collections, container, false); + } + + /** Called when the activity is first created. */ + @Override + public void onStart() { + super.onStart(); + + db = new Database(getActivity()); + + lvCollections = (ListView)getActivity().findViewById(R.id.lvCollections); + + swipeLayout = (SwipeRefreshLayout) getActivity().findViewById(R.id.swipe_container_collections); + swipeLayout.setOnRefreshListener(this); + swipeLayout.setColorScheme(R.color.light_blue, + R.color.blue, + R.color.dark_blue, + R.color.darkest_blue); + + CollectionAdapter collectionAdapter = new CollectionAdapter(getActivity(), create()); + + lvCollections.setAdapter(collectionAdapter); + + Cursor cur = collectionAdapter.getCursor(); + + lvCollections.setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView parent, View view, int position, long id) { + + CollectionAdapter adapter = (CollectionAdapter) parent.getAdapter(); + Cursor cur = adapter.getCursor(); + // Place the cursor at the selected item + if (cur.moveToPosition(position)) { + // and replace the cursor with one for the selected collection + ItemCollection coll = ItemCollection.load(cur); + if (coll != null && coll.getKey() != null) { + Intent i = new Intent(getActivity().getBaseContext(), ItemActivity.class); + if (coll.getSize() == 0) { + // Send a message that we need to refresh the collection + i.putExtra("com.mattrobertson.zotable.app.rerequest", true); + } + i.putExtra("com.mattrobertson.zotable.app.collectionKey", coll.getKey()); + startActivity(i); + } else { + // collection loaded was null. why? + Log.d(TAG, "Failed loading items for collection at position: "+position); + return; + } + } else { + // failed to move cursor-- show a toast + TextView tvTitle = (TextView)view.findViewById(R.id.collection_title); + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.collection_cant_open, tvTitle.getText()), + Toast.LENGTH_SHORT).show(); + return; + } + } + }); + + if (cur == null || cur.getCount() == 0) { + + if (!ServerCredentials.check(getActivity().getBaseContext())) { + Toast.makeText(getActivity().getBaseContext(), getResources().getString(R.string.sync_log_in_first),Toast.LENGTH_SHORT).show(); + return; + } + + Toast.makeText(getActivity(),getActivity().getResources().getString(R.string.no_collections),Toast.LENGTH_SHORT).show(); + Log.d(TAG, "Running a request to populate missing collections"); + + sync(); + } + + // Create floating action buttons + FloatingActionButton fabScan = (FloatingActionButton)getActivity().findViewById(R.id.btnFabScan); + FloatingActionButton fabIsbn = (FloatingActionButton)getActivity().findViewById(R.id.btnFabIsbn); + FloatingActionButton fabUpload = (FloatingActionButton)getActivity().findViewById(R.id.btnFabUpload); + FloatingActionButton fabManual = (FloatingActionButton)getActivity().findViewById(R.id.btnFabManual); + + fabScan.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + scanBarcode(Fragment_Collections.this); + } + }); + + fabIsbn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showDialog(DIALOG_IDENTIFIER); + } + }); + + fabUpload.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // This always works + Intent i = new Intent(getActivity(), FilePickerActivity.class); + // This works if you defined the intent filter + // Intent i = new Intent(Intent.ACTION_GET_CONTENT); + + // Set these depending on your use case. These are the defaults. + i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false); + i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); + i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE); + + // Configure initial directory by specifying a String. + // You could specify a String like "/storage/emulated/0/", but that can + // dangerous. Always use Android's API calls to get paths to the SD-card or + // internal memory. + String filepath = Environment.getExternalStorageDirectory().getPath(); + if (new File("/sdcard/").exists()) + filepath = "/sdcard/"; // prefer /sdcard... implemented this way in case DNE + + i.putExtra(FilePickerActivity.EXTRA_START_PATH, filepath); + + startActivityForResult(i, FILE_CODE); + } + }); + } + + public void onResume() { + CollectionAdapter adapter = (CollectionAdapter)(lvCollections.getAdapter()); + // XXX This may be too agressive-- fix if causes issues + Cursor newCursor = create(); + adapter.changeCursor(newCursor); + adapter.notifyDataSetChanged(); + if (db == null) db = new Database(getActivity()); + super.onResume(); + } + + public void onDestroy() { + CollectionAdapter adapter = (CollectionAdapter)(lvCollections.getAdapter()); + + if (adapter != null) { + Cursor cur = adapter.getCursor(); + + if (cur != null) + cur.close(); + } + + if (db != null) + db.close(); + + super.onDestroy(); + } + + @Override + public void onRefresh() { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + sync(); + } + }); + } + + /** + * Gives a cursor for top-level collections + * @return + */ + public Cursor create() { + String[] args = { "false" }; + Cursor cursor = db.query("collections", Database.COLLCOLS, "collection_parent=?", args, null, null, "collection_name", null); + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + + return cursor; + } + + public void showDialog(int id) { + switch (id) { + case DIALOG_NEW: + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(getResources().getString(R.string.item_type)) + // XXX i18n + .setItems(Item.ITEM_TYPES_EN, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int pos) { + Item item = new Item(getActivity().getBaseContext(), Item.ITEM_TYPES[pos]); + item.dirty = APIRequest.API_DIRTY; + item.save(db); + + Log.d(TAG, "Loading item data with key: " + item.getKey()); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getActivity().getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); + startActivity(i); + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + return; + case DIALOG_PROGRESS: + mProgressDialog = new ProgressDialog(getActivity()); + mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + mProgressDialog.setIndeterminate(true); + mProgressDialog.setMessage(getResources().getString(R.string.identifier_looking_up)); + mProgressDialog.show(); + return; + case DIALOG_IDENTIFIER: + final EditText input = new EditText(getActivity()); + input.setHint(getResources().getString(R.string.identifier_hint)); + input.setInputType(InputType.TYPE_CLASS_NUMBER); + + final Fragment_Collections current = this; + + dialog = new AlertDialog.Builder(getActivity()) + .setTitle(getResources().getString(R.string.identifier_message)) + .setView(input) + .setPositiveButton(getResources().getString(R.string.menu_search), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + Editable value = input.getText(); + // run search + Bundle c = new Bundle(); + c.putString("mode", "isbn"); + c.putString("identifier", value.toString()); + + // TODO: quick & dirty fix... check here if error occurs + Fragment_Collections.this.b = c; + + Log.d(TAG, b.getString("identifier")); + progressThread = new ProgressThread(handler, b); + progressThread.start(); + + showDialog(DIALOG_PROGRESS); + } + }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // do nothing + } + }).create(); + dialog.show(); + return; + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + Log.d(TAG, "_____________________on_activity_result"); + + if (requestCode == FILE_CODE) { + if (resultCode == Activity.RESULT_OK) { + Uri uri = intent.getData(); + Util.handleUpload(uri.getPath(),getActivity(),db,false); + } + } + else { + IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); + if (scanResult != null) { + // handle scan result + Bundle b = new Bundle(); + b.putString("mode", "isbn"); + b.putString("identifier", scanResult.getContents()); + + if (scanResult != null && scanResult.getContents() != null) { + Log.d(TAG, b.getString("identifier")); + progressThread = new ProgressThread(handler, b); + progressThread.start(); + showDialog(DIALOG_PROGRESS); + } else { + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_scan_failed), + Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_scan_failed), + Toast.LENGTH_SHORT).show(); + } + } + } + + final Handler handler = new Handler() { + public void handleMessage(Message msg) { + Log.d(TAG, "______________________handle_message"); + if (ProgressThread.STATE_DONE == msg.arg2) { + Bundle data = msg.getData(); + String itemKey = data.getString("itemKey"); + if (itemKey != null) { + + mProgressDialog.dismiss(); + mProgressDialog = null; + + Log.d(TAG, "Loading new item data with key: " + itemKey); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getActivity().getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", itemKey); + startActivity(i); + } + return; + } + + if (ProgressThread.STATE_PARSING == msg.arg2) { + if (mProgressDialog != null) + mProgressDialog.setMessage(getResources().getString(R.string.identifier_processing)); + return; + } + + if (ProgressThread.STATE_ERROR == msg.arg2) { + getActivity().dismissDialog(DIALOG_PROGRESS); + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_lookup_failed), + Toast.LENGTH_SHORT).show(); + progressThread.setState(ProgressThread.STATE_DONE); + return; + } + } + }; + + private class ProgressThread extends Thread { + Handler mHandler; + Bundle arguments; + final static int STATE_DONE = 5; + final static int STATE_FETCHING = 1; + final static int STATE_PARSING = 6; + final static int STATE_ERROR = 7; + + int mState; + + ProgressThread(Handler h, Bundle b) { + mHandler = h; + arguments = b; + Log.d(TAG, "_____________________thread_constructor"); + } + + public void run() { + Log.d(TAG, "_____________________thread_run"); + mState = STATE_FETCHING; + + // Setup + String identifier = arguments.getString("identifier"); + String mode = arguments.getString("mode"); + URL url; + String urlstring; + + String response = ""; + + if ("isbn".equals(mode)) { + urlstring = "http://xisbn.worldcat.org/webservices/xid/isbn/" + + identifier + + "?method=getMetadata&fl=*&format=json&count=1"; + } else { + urlstring = ""; + } + + try { + Log.d(TAG, "Fetching from: " + urlstring); + url = new URL(urlstring); + + /* Open a connection to that URL. */ + URLConnection ucon = url.openConnection(); + /* + * Define InputStreams to read from the URLConnection. + */ + InputStream is = ucon.getInputStream(); + BufferedInputStream bis = new BufferedInputStream(is, 16000); + + ByteArrayBuffer baf = new ByteArrayBuffer(50); + int current = 0; + + /* + * Read bytes to the Buffer until there is nothing more to read(-1). + */ + while (mState == STATE_FETCHING + && (current = bis.read()) != -1) { + baf.append((byte) current); + } + response = new String(baf.toByteArray()); + Log.d(TAG, response); + + + } catch (IOException e) { + Log.e(TAG, "Error: ", e); + } + + Message msg = mHandler.obtainMessage(); + msg.arg2 = STATE_PARSING; + mHandler.sendMessage(msg); + + /* + * { + "stat":"ok", + "list":[{ + "url":["http://www.worldcat.org/oclc/177669176?referer=xid"], + "publisher":"O'Reilly", + "form":["BA"], + "lccn":["2004273129"], + "lang":"eng", + "city":"Sebastopol, CA", + "author":"by Mark Lutz and David Ascher.", + "ed":"2nd ed.", + "year":"2003", + "isbn":["0596002815"], + "title":"Learning Python", + "oclcnum":["177669176", +.. + "748093898"]}]} + */ + + // This is OCLC-specific logic + try { + JSONObject result = new JSONObject(response); + + if (!result.getString("stat").equals("ok")) { + Log.e(TAG, "Error response received"); + msg = mHandler.obtainMessage(); + msg.arg2 = STATE_ERROR; + mHandler.sendMessage(msg); + return; + } + + result = result.getJSONArray("list").getJSONObject(0); + String form = result.getJSONArray("form").getString(0); + String type; + + if ("AA".equals(form)) type = "audioRecording"; + else if ("VA".equals(form)) type = "videoRecording"; + else if ("FA".equals(form)) type = "film"; + else type = "book"; + + // TODO Fix this + type = "book"; + + Item item = new Item(getActivity().getBaseContext(), type); + + JSONObject content = item.getContent(); + + if (result.has("lccn")) { + String lccn = "LCCN: " + result.getJSONArray("lccn").getString(0); + content.put("extra", lccn); + } + + if (result.has("isbn")) { + content.put("ISBN", result.getJSONArray("isbn").getString(0)); + } + + content.put("title", result.optString("title", "")); + content.put("place", result.optString("city", "")); + content.put("edition", result.optString("ed", "")); + content.put("language", result.optString("lang", "")); + content.put("publisher", result.optString("publisher", "")); + content.put("date", result.optString("year", "")); + + item.setTitle(result.optString("title", "")); + item.setYear(result.optString("year", "")); + + String author = result.optString("author", ""); + + item.setCreatorSummary(author); + JSONArray array = new JSONArray(); + JSONObject member = new JSONObject(); + member.accumulate("creatorType", "author"); + member.accumulate("name", author); + array.put(member); + content.put("creators", array); + + item.setContent(content); // NOTE: tags item as DIRTY + item.save(db); + + msg = mHandler.obtainMessage(); + Bundle data = new Bundle(); + data.putString("itemKey", item.getKey()); + msg.setData(data); + msg.arg2 = STATE_DONE; + mHandler.sendMessage(msg); + return; + } catch (JSONException e) { + Log.e(TAG, "exception parsing response", e); + msg = mHandler.obtainMessage(); + msg.arg2 = STATE_ERROR; + mHandler.sendMessage(msg); + return; + } + } + + public void setState(int state) { + mState = state; + } + } + + public void sync() { + // Check log-in -- prompt if not logged in + if (!ServerCredentials.check(getActivity().getBaseContext())) { + Toast.makeText(getActivity().getBaseContext(), getResources().getString(R.string.sync_log_in_first),Toast.LENGTH_SHORT).show(); + return; + } + + Log.d(TAG, "Making sync request for all collections"); + + // Get credentials + ServerCredentials cred = new ServerCredentials(getActivity().getBaseContext()); + APIRequest req = APIRequest.fetchCollections(cred); + req.setHandler(mEvent); + + new ZoteroAPITask(getActivity().getBaseContext()).execute(req); + } + + public void scanBarcode(Fragment_Collections current) { + // If we're about to download from Google play, cancel that dialog + // and prompt from Amazon if we're on an Amazon device + FragmentIntentIntegrator integrator = new FragmentIntentIntegrator(this); + AlertDialog producedDialog = integrator.initiateScan(); + if (producedDialog != null && "amazon".equals(BuildConfig.FLAVOR)) { + producedDialog.dismiss(); + AmazonZxingGlue.showDownloadDialog(current.getActivity()); + } + } + + private final class FragmentIntentIntegrator extends IntentIntegrator { + + private final Fragment fragment; + + public FragmentIntentIntegrator(Fragment fragment) { + super(fragment.getActivity()); + this.fragment = fragment; + } + + @Override + protected void startActivityForResult(Intent intent, int code) { + fragment.startActivityForResult(intent, code); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mattrobertson/zotable/app/Fragment_Favorites.java b/src/main/java/com/mattrobertson/zotable/app/Fragment_Favorites.java new file mode 100755 index 0000000..5c81827 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/Fragment_Favorites.java @@ -0,0 +1,585 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Message; +import android.support.v4.app.Fragment; +import android.support.v4.app.ListFragment; +import android.text.Editable; +import android.text.InputType; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import com.getbase.floatingactionbutton.FloatingActionButton; +import com.google.zxing.integration.android.IntentIntegrator; +import com.google.zxing.integration.android.IntentResult; +import com.mattrobertson.zotable.app.data.CollectionAdapter; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemCollection; +import com.mattrobertson.zotable.app.task.APIRequest; +import com.nononsenseapps.filepicker.FilePickerActivity; + +import org.apache.http.util.ByteArrayBuffer; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; + +public class Fragment_Favorites extends ListFragment { + + private static final String TAG = "Fragment_Favorites"; + + static final int DIALOG_NEW = 1; + static final int DIALOG_SORT = 2; + static final int DIALOG_IDENTIFIER = 3; + static final int DIALOG_PROGRESS = 6; + + static final int FILE_CODE = 21; + + Bundle b = new Bundle(); + + private ProgressDialog mProgressDialog; + private ProgressThread progressThread; + + private Database db; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_favorites, container, false); + } + + /** Called when the activity is first created. */ + @Override + public void onStart() { + super.onStart(); + + db = new Database(getActivity()); + + CollectionAdapter collectionAdapter = new CollectionAdapter(getActivity(), create()); + + setListAdapter(collectionAdapter); + + ListView lv = getListView(); + + lv.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + CollectionAdapter adapter = (CollectionAdapter) parent.getAdapter(); + Cursor cur = adapter.getCursor(); + // Place the cursor at the selected item + if (cur.moveToPosition(position)) { + // and open activity for the selected collection + ItemCollection coll = ItemCollection.load(cur); + if (coll != null && coll.getKey() != null && coll.getSubcollections(db).size() > 0) { + Log.d(TAG, "Loading child collection with key: "+coll.getKey()); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getActivity().getBaseContext(), CollectionActivity.class); + i.putExtra("com.mattrobertson.zotable.app.collectionKey", coll.getKey()); + startActivity(i); + } else { + Log.d(TAG, "Failed loading child collections for collection"); + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.collection_no_subcollections), + Toast.LENGTH_SHORT).show(); + } + } else { + // failed to move cursor-- show a toast + TextView tvTitle = (TextView)view.findViewById(R.id.collection_title); + Toast.makeText(getActivity().getApplicationContext(),getResources().getString(R.string.collection_cant_open, tvTitle.getText()),Toast.LENGTH_SHORT).show(); + } + return true; + } + }); + + lv.setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView parent, View view, int position, long id) { + + CollectionAdapter adapter = (CollectionAdapter) parent.getAdapter(); + Cursor cur = adapter.getCursor(); + // Place the cursor at the selected item + if (cur.moveToPosition(position)) { + // and replace the cursor with one for the selected collection + ItemCollection coll = ItemCollection.load(cur); + if (coll != null && coll.getKey() != null) { + Intent i = new Intent(getActivity().getBaseContext(), ItemActivity.class); + if (coll.getSize() == 0) { + // Send a message that we need to refresh the collection + i.putExtra("com.mattrobertson.zotable.app.rerequest", true); + } + i.putExtra("com.mattrobertson.zotable.app.collectionKey", coll.getKey()); + startActivity(i); + } else { + // collection loaded was null. why? + Log.d(TAG, "Failed loading items for collection at position: " + position); + } + } else { + // failed to move cursor-- show a toast + TextView tvTitle = (TextView) view.findViewById(R.id.collection_title); + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.collection_cant_open, tvTitle.getText()), + Toast.LENGTH_SHORT).show(); + } + } + }); + + // Create floating action buttons + FloatingActionButton fabScan = (FloatingActionButton)getActivity().findViewById(R.id.btnFabScan); + FloatingActionButton fabIsbn = (FloatingActionButton)getActivity().findViewById(R.id.btnFabIsbn); + FloatingActionButton fabUpload = (FloatingActionButton)getActivity().findViewById(R.id.btnFabUpload); + FloatingActionButton fabManual = (FloatingActionButton)getActivity().findViewById(R.id.btnFabManual); + + fabScan.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + scanBarcode(Fragment_Favorites.this); + } + }); + + fabIsbn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showDialog(DIALOG_IDENTIFIER); + } + }); + + fabUpload.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // This always works + Intent i = new Intent(getActivity(), FilePickerActivity.class); + // This works if you defined the intent filter + // Intent i = new Intent(Intent.ACTION_GET_CONTENT); + + // Set these depending on your use case. These are the defaults. + i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false); + i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); + i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE); + + // Configure initial directory by specifying a String. + // You could specify a String like "/storage/emulated/0/", but that can + // dangerous. Always use Android's API calls to get paths to the SD-card or + // internal memory. + String filepath = Environment.getExternalStorageDirectory().getPath(); + if (new File("/sdcard/").exists()) + filepath = "/sdcard/"; // prefer /sdcard... implemented this way in case DNE + + i.putExtra(FilePickerActivity.EXTRA_START_PATH, filepath); + + startActivityForResult(i, FILE_CODE); + } + }); + } + + public void onResume() { + CollectionAdapter adapter = (CollectionAdapter) getListAdapter(); + // XXX This may be too agressive-- fix if causes issues + Cursor newCursor = create(); + adapter.changeCursor(newCursor); + adapter.notifyDataSetChanged(); + if (db == null) db = new Database(getActivity()); + super.onResume(); + } + + public void onDestroy() { + CollectionAdapter adapter = (CollectionAdapter) getListAdapter(); + + if (adapter != null) { + Cursor cur = adapter.getCursor(); + + if (cur != null) + cur.close(); + } + + if (db != null) + db.close(); + + super.onDestroy(); + } + + /** + * Gives a cursor for top-level collections + */ + public Cursor create() { + String[] args = {"1"}; + Cursor cursor = db.query("collections", Database.COLLCOLS, "fav=?", args, null, null, "collection_name", null); + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + + return cursor; + } + + public void showDialog(int id) { + switch (id) { + case DIALOG_NEW: + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(getResources().getString(R.string.item_type)) + // XXX i18n + .setItems(Item.ITEM_TYPES_EN, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int pos) { + Item item = new Item(getActivity().getBaseContext(), Item.ITEM_TYPES[pos]); + item.dirty = APIRequest.API_DIRTY; + item.save(db); + + Log.d(TAG, "Loading item data with key: " + item.getKey()); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getActivity().getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); + startActivity(i); + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + return; + case DIALOG_PROGRESS: + mProgressDialog = new ProgressDialog(getActivity()); + mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + mProgressDialog.setIndeterminate(true); + mProgressDialog.setMessage(getResources().getString(R.string.identifier_looking_up)); + mProgressDialog.show(); + return; + case DIALOG_IDENTIFIER: + final EditText input = new EditText(getActivity()); + input.setHint(getResources().getString(R.string.identifier_hint)); + input.setInputType(InputType.TYPE_CLASS_NUMBER); + + final Fragment_Favorites current = this; + + dialog = new AlertDialog.Builder(getActivity()) + .setTitle(getResources().getString(R.string.identifier_message)) + .setView(input) + .setPositiveButton(getResources().getString(R.string.menu_search), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + Editable value = input.getText(); + // run search + Bundle c = new Bundle(); + c.putString("mode", "isbn"); + c.putString("identifier", value.toString()); + + // TODO: quick & dirty fix... check here if error occurs + Fragment_Favorites.this.b = c; + + Log.d(TAG, b.getString("identifier")); + progressThread = new ProgressThread(handler, b); + progressThread.start(); + + showDialog(DIALOG_PROGRESS); + } + }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // do nothing + } + }).create(); + dialog.show(); + return; + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + Log.d(TAG, "_____________________on_activity_result"); + + if (requestCode == FILE_CODE) { + if (resultCode == Activity.RESULT_OK) { + Uri uri = intent.getData(); + Util.handleUpload(uri.getPath(),getActivity(),db,false); + } + } + else { + IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); + if (scanResult != null) { + // handle scan result + Bundle b = new Bundle(); + b.putString("mode", "isbn"); + b.putString("identifier", scanResult.getContents()); + + if (scanResult != null && scanResult.getContents() != null) { + Log.d(TAG, b.getString("identifier")); + progressThread = new ProgressThread(handler, b); + progressThread.start(); + showDialog(DIALOG_PROGRESS); + } else { + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_scan_failed), + Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_scan_failed), + Toast.LENGTH_SHORT).show(); + } + } + } + + final Handler handler = new Handler() { + public void handleMessage(Message msg) { + Log.d(TAG, "______________________handle_message"); + if (ProgressThread.STATE_DONE == msg.arg2) { + Bundle data = msg.getData(); + String itemKey = data.getString("itemKey"); + if (itemKey != null) { + + mProgressDialog.dismiss(); + mProgressDialog = null; + + Log.d(TAG, "Loading new item data with key: " + itemKey); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getActivity().getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", itemKey); + startActivity(i); + } + return; + } + + if (ProgressThread.STATE_PARSING == msg.arg2) { + if (mProgressDialog != null) + mProgressDialog.setMessage(getResources().getString(R.string.identifier_processing)); + return; + } + + if (ProgressThread.STATE_ERROR == msg.arg2) { + getActivity().dismissDialog(DIALOG_PROGRESS); + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_lookup_failed), + Toast.LENGTH_SHORT).show(); + progressThread.setState(ProgressThread.STATE_DONE); + return; + } + } + }; + + private class ProgressThread extends Thread { + Handler mHandler; + Bundle arguments; + final static int STATE_DONE = 5; + final static int STATE_FETCHING = 1; + final static int STATE_PARSING = 6; + final static int STATE_ERROR = 7; + + int mState; + + ProgressThread(Handler h, Bundle b) { + mHandler = h; + arguments = b; + Log.d(TAG, "_____________________thread_constructor"); + } + + public void run() { + Log.d(TAG, "_____________________thread_run"); + mState = STATE_FETCHING; + + // Setup + String identifier = arguments.getString("identifier"); + String mode = arguments.getString("mode"); + URL url; + String urlstring; + + String response = ""; + + if ("isbn".equals(mode)) { + urlstring = "http://xisbn.worldcat.org/webservices/xid/isbn/" + + identifier + + "?method=getMetadata&fl=*&format=json&count=1"; + } else { + urlstring = ""; + } + + try { + Log.d(TAG, "Fetching from: " + urlstring); + url = new URL(urlstring); + + /* Open a connection to that URL. */ + URLConnection ucon = url.openConnection(); + /* + * Define InputStreams to read from the URLConnection. + */ + InputStream is = ucon.getInputStream(); + BufferedInputStream bis = new BufferedInputStream(is, 16000); + + ByteArrayBuffer baf = new ByteArrayBuffer(50); + int current = 0; + + /* + * Read bytes to the Buffer until there is nothing more to read(-1). + */ + while (mState == STATE_FETCHING + && (current = bis.read()) != -1) { + baf.append((byte) current); + } + response = new String(baf.toByteArray()); + Log.d(TAG, response); + + + } catch (IOException e) { + Log.e(TAG, "Error: ", e); + } + + Message msg = mHandler.obtainMessage(); + msg.arg2 = STATE_PARSING; + mHandler.sendMessage(msg); + + /* + * { + "stat":"ok", + "list":[{ + "url":["http://www.worldcat.org/oclc/177669176?referer=xid"], + "publisher":"O'Reilly", + "form":["BA"], + "lccn":["2004273129"], + "lang":"eng", + "city":"Sebastopol, CA", + "author":"by Mark Lutz and David Ascher.", + "ed":"2nd ed.", + "year":"2003", + "isbn":["0596002815"], + "title":"Learning Python", + "oclcnum":["177669176", +.. + "748093898"]}]} + */ + + // This is OCLC-specific logic + try { + JSONObject result = new JSONObject(response); + + if (!result.getString("stat").equals("ok")) { + Log.e(TAG, "Error response received"); + msg = mHandler.obtainMessage(); + msg.arg2 = STATE_ERROR; + mHandler.sendMessage(msg); + return; + } + + result = result.getJSONArray("list").getJSONObject(0); + String form = result.getJSONArray("form").getString(0); + String type; + + if ("AA".equals(form)) type = "audioRecording"; + else if ("VA".equals(form)) type = "videoRecording"; + else if ("FA".equals(form)) type = "film"; + else type = "book"; + + // TODO Fix this + type = "book"; + + Item item = new Item(getActivity().getBaseContext(), type); + + JSONObject content = item.getContent(); + + if (result.has("lccn")) { + String lccn = "LCCN: " + result.getJSONArray("lccn").getString(0); + content.put("extra", lccn); + } + + if (result.has("isbn")) { + content.put("ISBN", result.getJSONArray("isbn").getString(0)); + } + + content.put("title", result.optString("title", "")); + content.put("place", result.optString("city", "")); + content.put("edition", result.optString("ed", "")); + content.put("language", result.optString("lang", "")); + content.put("publisher", result.optString("publisher", "")); + content.put("date", result.optString("year", "")); + + item.setTitle(result.optString("title", "")); + item.setYear(result.optString("year", "")); + + String author = result.optString("author", ""); + + item.setCreatorSummary(author); + JSONArray array = new JSONArray(); + JSONObject member = new JSONObject(); + member.accumulate("creatorType", "author"); + member.accumulate("name", author); + array.put(member); + content.put("creators", array); + + item.setContent(content); // NOTE: tags item as DIRTY + item.save(db); + + msg = mHandler.obtainMessage(); + Bundle data = new Bundle(); + data.putString("itemKey", item.getKey()); + msg.setData(data); + msg.arg2 = STATE_DONE; + mHandler.sendMessage(msg); + return; + } catch (JSONException e) { + Log.e(TAG, "exception parsing response", e); + msg = mHandler.obtainMessage(); + msg.arg2 = STATE_ERROR; + mHandler.sendMessage(msg); + return; + } + } + + public void setState(int state) { + mState = state; + } + } + + public void scanBarcode(Fragment_Favorites current) { + // If we're about to download from Google play, cancel that dialog + // and prompt from Amazon if we're on an Amazon device + FragmentIntentIntegrator integrator = new FragmentIntentIntegrator(this); + AlertDialog producedDialog = integrator.initiateScan(); + if (producedDialog != null && "amazon".equals(BuildConfig.FLAVOR)) { + producedDialog.dismiss(); + AmazonZxingGlue.showDownloadDialog(current.getActivity()); + } + } + + private final class FragmentIntentIntegrator extends IntentIntegrator { + + private final Fragment fragment; + + public FragmentIntentIntegrator(Fragment fragment) { + super(fragment.getActivity()); + this.fragment = fragment; + } + + @Override + protected void startActivityForResult(Intent intent, int code) { + fragment.startActivityForResult(intent, code); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mattrobertson/zotable/app/Fragment_Items.java b/src/main/java/com/mattrobertson/zotable/app/Fragment_Items.java new file mode 100755 index 0000000..85180eb --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/Fragment_Items.java @@ -0,0 +1,915 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Message; +import android.support.v4.app.Fragment; +import android.support.v4.app.ListFragment; +import android.support.v4.widget.SwipeRefreshLayout; +import android.text.Editable; +import android.text.InputType; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import com.getbase.floatingactionbutton.FloatingActionButton; + + +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemAdapter; +import com.mattrobertson.zotable.app.data.ItemCollection; +import com.mattrobertson.zotable.app.task.APIEvent; +import com.mattrobertson.zotable.app.task.APIRequest; +import com.mattrobertson.zotable.app.task.ZoteroAPITask; +import com.google.zxing.integration.android.IntentIntegrator; +import com.google.zxing.integration.android.IntentResult; +import com.nononsenseapps.filepicker.FilePickerActivity; +import com.squareup.otto.Subscribe; + +import org.apache.http.util.ByteArrayBuffer; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; + +public class Fragment_Items extends ListFragment implements SwipeRefreshLayout.OnRefreshListener { + + private static final String TAG = "Fragment_Items"; + + static final int DIALOG_NEW = 1; + static final int DIALOG_SORT = 2; + static final int DIALOG_IDENTIFIER = 3; + static final int DIALOG_PROGRESS = 6; + + static final int FILE_CODE = 21; + + Bundle b = new Bundle(); + + /** + * Allowed sort orderings + */ + static final String[] SORTS = { + "item_year, item_title COLLATE NOCASE", + "item_creator COLLATE NOCASE, item_year", + "item_title COLLATE NOCASE, item_year", + "timestamp ASC, item_title COLLATE NOCASE" + }; + + /** + * Strings providing the names of each ordering, respectively + */ + static final int[] SORT_NAMES = { + R.string.sort_year_title, + R.string.sort_creator_year, + R.string.sort_title_year, + R.string.sort_modified_title + }; + private static final String SORT_CHOICE = "sort_choice"; + + private Database db; + + private SwipeRefreshLayout swipeLayout; + + private ProgressDialog mProgressDialog; + private ProgressThread progressThread; + + public String sortBy = "item_year, item_title"; + + final Handler syncHandler = new Handler() { + public void handleMessage(Message msg) { + Log.d(TAG, "received message: " + msg.arg1); + refreshView(); + + if (msg.arg1 == APIRequest.UPDATED_DATA) { + refreshView(); + return; + } + + if (msg.arg1 == APIRequest.QUEUED_MORE) { + //Toast.makeText(getActivity().getApplicationContext(),getResources().getString(R.string.sync_queued_more, msg.arg2),Toast.LENGTH_SHORT).show(); + return; + } + + if (msg.arg1 == APIRequest.BATCH_DONE) { + Application.getInstance().getBus().post(SyncEvent.COMPLETE); + + // Sync success w/o erros -- set all items to clean status in DB + Item.setAllClean(db); + + Toast.makeText(getActivity().getApplicationContext(),getResources().getString(R.string.sync_complete),Toast.LENGTH_SHORT).show(); + return; + } + + if (msg.arg1 == APIRequest.ERROR_UNKNOWN) { + Toast.makeText(getActivity().getApplicationContext(),getResources().getString(R.string.sync_error),Toast.LENGTH_SHORT).show(); + return; + } + } + }; + + private APIEvent mEvent = new APIEvent() { + private int updates = 0; + + @Override + public void onComplete(APIRequest request) { + Message msg = syncHandler.obtainMessage(); + msg.arg1 = APIRequest.BATCH_DONE; + syncHandler.sendMessage(msg); + Log.d(TAG, "fired oncomplete"); + + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + // Stop the Spinning refreshing icon + swipeLayout.setRefreshing(false); + } + }); + } + + @Override + public void onUpdate(APIRequest request) { + updates++; + + if (updates % 10 == 0) { + Message msg = syncHandler.obtainMessage(); + msg.arg1 = APIRequest.UPDATED_DATA; + syncHandler.sendMessage(msg); + } else { + // do nothing + } + } + + @Override + public void onError(APIRequest request, Exception exception) { + Log.e(TAG, "APIException caught", exception); + Message msg = syncHandler.obtainMessage(); + msg.arg1 = APIRequest.ERROR_UNKNOWN; + syncHandler.sendMessage(msg); + } + + @Override + public void onError(APIRequest request, int error) { + Log.e(TAG, "API error caught"); + Message msg = syncHandler.obtainMessage(); + msg.arg1 = APIRequest.ERROR_UNKNOWN; + syncHandler.sendMessage(msg); + } + }; + + /** + * Refreshes the current list adapter + */ + private void refreshView() { + ItemAdapter adapter = (ItemAdapter) getListAdapter(); + if (adapter == null) return; + + Cursor newCursor = prepareCursor(); + adapter.changeCursor(newCursor); + adapter.notifyDataSetChanged(); + Log.d(TAG, "refreshing view on request"); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_items, container, false); + } + + /** + * Called when the activity is first created. + */ + @Override + public void onStart() { + super.onStart(); + + setHasOptionsMenu(true); + + String persistedSort = Persistence.read(SORT_CHOICE); + if (persistedSort != null) sortBy = persistedSort; + + db = new Database(getActivity()); + + swipeLayout = (SwipeRefreshLayout) getActivity().findViewById(R.id.swipe_container_items); + swipeLayout.setOnRefreshListener(this); + swipeLayout.setColorScheme(R.color.light_blue, + R.color.blue, + R.color.dark_blue, + R.color.darkest_blue); + + APIRequest req; + + req = APIRequest.fetchItems(false, new ServerCredentials(getActivity())); + + prepareAdapter(); + + ItemAdapter adapter = (ItemAdapter) getListAdapter(); + Cursor cur = adapter.getCursor(); + + // TODO: quick & dirty hack -- possible error here? compare with github original + if (cur == null || cur.getCount() == 0) { + + if (!ServerCredentials.check(getActivity().getBaseContext())) { + Toast.makeText(getActivity().getBaseContext(), getResources().getString(R.string.sync_log_in_first), + Toast.LENGTH_SHORT).show(); + return; + } + + Toast.makeText(getActivity(),getActivity().getResources().getString(R.string.collection_empty),Toast.LENGTH_SHORT).show(); + Log.d(TAG, "Running a request to populate missing items"); + ZoteroAPITask task = new ZoteroAPITask(getActivity()); + req.setHandler(mEvent); + task.execute(req); + + } + + final ListView lv = getListView(); + lv.setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView parent, View view, int position, long id) { + // If we have a click on an item, do something... + ItemAdapter adapter = (ItemAdapter) parent.getAdapter(); + Cursor cur = adapter.getCursor(); + // Place the cursor at the selected item + if (cur.moveToPosition(position)) { + // and load an activity for the item + Item item = Item.load(cur); + + Log.d(TAG, "Loading item data with key: " + item.getKey()); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getActivity().getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); + i.putExtra("com.mattrobertson.zotable.app.itemDbId", item.dbId); + startActivity(i); + } else { + // failed to move cursor-- show a toast + TextView tvTitle = (TextView) view.findViewById(R.id.item_title); + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.cant_open_item, tvTitle.getText()), + Toast.LENGTH_SHORT).show(); + } + } + }); + + lv.setOnScrollListener(new AbsListView.OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView absListView, int i) {} + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + int topRowVerticalPosition = (lv == null || lv.getChildCount() == 0) ? 0 : lv.getChildAt(0).getTop(); + + swipeLayout.setEnabled(firstVisibleItem == 0 && topRowVerticalPosition >= 0); + } + }); + + // Create floating action buttons + FloatingActionButton fabScan = (FloatingActionButton)getActivity().findViewById(R.id.btnFabScan); + FloatingActionButton fabIsbn = (FloatingActionButton)getActivity().findViewById(R.id.btnFabIsbn); + FloatingActionButton fabUpload = (FloatingActionButton)getActivity().findViewById(R.id.btnFabUpload); + FloatingActionButton fabManual = (FloatingActionButton)getActivity().findViewById(R.id.btnFabManual); + + fabScan.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + scanBarcode(Fragment_Items.this); + } + }); + + fabIsbn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showDialog(DIALOG_IDENTIFIER); + } + }); + + fabUpload.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // This always works + Intent i = new Intent(getActivity(), FilePickerActivity.class); + // This works if you defined the intent filter + // Intent i = new Intent(Intent.ACTION_GET_CONTENT); + + // Set these depending on your use case. These are the defaults. + i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false); + i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); + i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE); + + // Configure initial directory by specifying a String. + // You could specify a String like "/storage/emulated/0/", but that can + // dangerous. Always use Android's API calls to get paths to the SD-card or + // internal memory. + String filepath = Environment.getExternalStorageDirectory().getPath(); + if (new File("/sdcard/").exists()) + filepath = "/sdcard/"; // prefer /sdcard... implemented this way in case DNE + + i.putExtra(FilePickerActivity.EXTRA_START_PATH, filepath); + + startActivityForResult(i, FILE_CODE); + } + }); + + fabManual.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showDialog(DIALOG_NEW); + } + }); + } + + @Override + public void onResume() { + super.onResume(); + Application.getInstance().getBus().register(this); + refreshView(); + } + + @Override + public void onDestroy() { + ItemAdapter adapter = (ItemAdapter) getListAdapter(); + Cursor cur = null; + + if (adapter != null) + cur = adapter.getCursor(); + + if (cur != null) + cur.close(); + + if (db != null) + db.close(); + + super.onDestroy(); + } + + @Override + public void onPause() { + super.onPause(); + Application.getInstance().getBus().unregister(this); + } + + @Override + public void onRefresh() { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + sync(); + } + }); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + + menu.clear(); + + inflater.inflate(R.menu.zotero_menu,menu); + + // Turn on sort item + MenuItem sort = menu.findItem(R.id.do_sort); + sort.setEnabled(true); + sort.setVisible(true); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + switch (item.getItemId()) { + case R.id.do_sort: + showDialog(DIALOG_SORT); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + private void prepareAdapter() { + ItemAdapter adapter = new ItemAdapter(getActivity(), prepareCursor()); + setListAdapter(adapter); + } + + private Cursor prepareCursor() { + + // No longer searchable because it is a fragment... + return getCursor(); +/* + Cursor cursor; + // Be ready for a search + Intent intent = getActivity().getIntent(); + + if (Intent.ACTION_SEARCH.equals(intent.getAction())) { + query = intent.getStringExtra(SearchManager.QUERY); + cursor = getCursor(query); + getActivity().setTitle(getResources().getString(R.string.search_results, query)); + } else if (query != null) { + cursor = getCursor(query); + getActivity().setTitle(getResources().getString(R.string.search_results, query)); + } else if (intent.getStringExtra("com.mattrobertson.zotable.app.tag") != null) { + String tag = intent.getStringExtra("com.mattrobertson.zotable.app.tag"); + Query q = new Query(); + q.set("tag", tag); + cursor = getCursor(q); + getActivity().setTitle(getResources().getString(R.string.tag_viewing_items, tag)); + } else { + collectionKey = intent.getStringExtra("com.mattrobertson.zotable.app.collectionKey"); + + ItemCollection coll; + + if (collectionKey != null && (coll = ItemCollection.load(collectionKey, db)) != null) { + cursor = getCursor(coll); + getActivity().setTitle(coll.getTitle()); + } else { + cursor = getCursor(); + getActivity().setTitle(getResources().getString(R.string.all_items)); + } + } + return cursor; +*/ + } + + public void showDialog(int id) { + switch (id) { + case DIALOG_NEW: + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(getResources().getString(R.string.item_type)) + // XXX i18n + .setItems(Item.ITEM_TYPES_EN, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int pos) { + Item item = new Item(getActivity().getBaseContext(), Item.ITEM_TYPES[pos]); + item.dirty = APIRequest.API_DIRTY; + item.save(db); + + Log.d(TAG, "Loading item data with key: " + item.getKey()); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getActivity().getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); + startActivity(i); + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + return; + case DIALOG_SORT: + + // We generate the sort name list for our current locale + String[] sorts = new String[SORT_NAMES.length]; + for (int j = 0; j < SORT_NAMES.length; j++) { + sorts[j] = getResources().getString(SORT_NAMES[j]); + } + + AlertDialog.Builder builder2 = new AlertDialog.Builder(getActivity()); + builder2.setTitle(getResources().getString(R.string.set_sort_order)) + .setItems(sorts, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int pos) { + Cursor cursor = getCursor(); + setSortBy(SORTS[pos]); + + ItemAdapter adapter = (ItemAdapter) getListAdapter(); + adapter.changeCursor(cursor); + Log.d(TAG, "Re-sorting by: " + SORTS[pos]); + + Persistence.write(SORT_CHOICE, SORTS[pos]); + } + }); + AlertDialog dialog2 = builder2.create(); + dialog2.show(); + return; + case DIALOG_PROGRESS: + mProgressDialog = new ProgressDialog(getActivity()); + mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + mProgressDialog.setIndeterminate(true); + mProgressDialog.setMessage(getResources().getString(R.string.identifier_looking_up)); + mProgressDialog.show(); + return; + case DIALOG_IDENTIFIER: + final EditText input = new EditText(getActivity()); + input.setHint(getResources().getString(R.string.identifier_hint)); + input.setInputType(InputType.TYPE_CLASS_NUMBER); + + final Fragment_Items current = this; + + dialog = new AlertDialog.Builder(getActivity()) + .setTitle(getResources().getString(R.string.identifier_message)) + .setView(input) + .setPositiveButton(getResources().getString(R.string.menu_search), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + Editable value = input.getText(); + // run search + Bundle c = new Bundle(); + c.putString("mode", "isbn"); + c.putString("identifier", value.toString()); + + // TODO: quick & dirty fix... check here if error occurs + Fragment_Items.this.b = c; + + Log.d(TAG, b.getString("identifier")); + progressThread = new ProgressThread(handler, b); + progressThread.start(); + + showDialog(DIALOG_PROGRESS); + } + }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // do nothing + } + }).create(); + dialog.show(); + return; + } + } + + /* Sorting */ + public void setSortBy(String sort) { + this.sortBy = sort; + } + + /* Handling the ListView and keeping it up to date */ + public Cursor getCursor() { + Cursor cursor = db.query("items", Database.ITEMCOLS, null, null, null, null, this.sortBy, null); + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + return cursor; + } + + public Cursor getCursor(ItemCollection parent) { + String[] args = {parent.dbId}; + Cursor cursor = db.rawQuery("SELECT item_title, item_type, item_content, etag, dirty, " + + "items._id, item_key, item_year, item_creator, timestamp, item_children " + + " FROM items, itemtocollections WHERE items._id = item_id AND collection_id=? ORDER BY " + this.sortBy, + args); + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + return cursor; + } + + public Cursor getCursor(String query) { + String[] args = {"%" + query + "%", "%" + query + "%"}; + Cursor cursor = db.rawQuery("SELECT item_title, item_type, item_content, etag, dirty, " + + "_id, item_key, item_year, item_creator, timestamp, item_children " + + " FROM items WHERE item_title LIKE ? OR item_creator LIKE ?" + + " ORDER BY " + this.sortBy, + args); + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + return cursor; + } + + public Cursor getCursor(Query query) { + return query.query(db); + } + + @Subscribe + public void syncComplete(SyncEvent event) { + if (event.getStatus() == SyncEvent.COMPLETE_CODE) refreshView(); + } + + /* Thread and helper to run lookups */ + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + Log.d(TAG, "_____________________on_activity_result"); + + if (requestCode == FILE_CODE) { + if (resultCode == Activity.RESULT_OK) { + Uri uri = intent.getData(); + Util.handleUpload(uri.getPath(),getActivity(),db,false); + } + } + else { + IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); + if (scanResult != null) { + // handle scan result + Bundle b = new Bundle(); + b.putString("mode", "isbn"); + b.putString("identifier", scanResult.getContents()); + + if (scanResult != null && scanResult.getContents() != null) { + Log.d(TAG, b.getString("identifier")); + progressThread = new ProgressThread(handler, b); + progressThread.start(); + showDialog(DIALOG_PROGRESS); + } else { + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_scan_failed), + Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_scan_failed), + Toast.LENGTH_SHORT).show(); + } + } + } + + final Handler handler = new Handler() { + public void handleMessage(Message msg) { + Log.d(TAG, "______________________handle_message"); + if (ProgressThread.STATE_DONE == msg.arg2) { + Bundle data = msg.getData(); + String itemKey = data.getString("itemKey"); + if (itemKey != null) { + + mProgressDialog.dismiss(); + mProgressDialog = null; + + Log.d(TAG, "Loading new item data with key: " + itemKey); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getActivity().getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", itemKey); + startActivity(i); + } + return; + } + + if (ProgressThread.STATE_PARSING == msg.arg2) { + if (mProgressDialog != null) + mProgressDialog.setMessage(getResources().getString(R.string.identifier_processing)); + return; + } + + if (ProgressThread.STATE_ERROR == msg.arg2) { + getActivity().dismissDialog(DIALOG_PROGRESS); + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_lookup_failed), + Toast.LENGTH_SHORT).show(); + progressThread.setState(ProgressThread.STATE_DONE); + return; + } + } + }; + + private class ProgressThread extends Thread { + Handler mHandler; + Bundle arguments; + final static int STATE_DONE = 5; + final static int STATE_FETCHING = 1; + final static int STATE_PARSING = 6; + final static int STATE_ERROR = 7; + + int mState; + + ProgressThread(Handler h, Bundle b) { + mHandler = h; + arguments = b; + Log.d(TAG, "_____________________thread_constructor"); + } + + public void run() { + Log.d(TAG, "_____________________thread_run"); + mState = STATE_FETCHING; + + // Setup + String identifier = arguments.getString("identifier"); + String mode = arguments.getString("mode"); + URL url; + String urlstring; + + String response = ""; + + if ("isbn".equals(mode)) { + urlstring = "http://xisbn.worldcat.org/webservices/xid/isbn/" + + identifier + + "?method=getMetadata&fl=*&format=json&count=1"; + } else { + urlstring = ""; + } + + try { + Log.d(TAG, "Fetching from: " + urlstring); + url = new URL(urlstring); + + /* Open a connection to that URL. */ + URLConnection ucon = url.openConnection(); + /* + * Define InputStreams to read from the URLConnection. + */ + InputStream is = ucon.getInputStream(); + BufferedInputStream bis = new BufferedInputStream(is, 16000); + + ByteArrayBuffer baf = new ByteArrayBuffer(50); + int current = 0; + + /* + * Read bytes to the Buffer until there is nothing more to read(-1). + */ + while (mState == STATE_FETCHING + && (current = bis.read()) != -1) { + baf.append((byte) current); + } + response = new String(baf.toByteArray()); + Log.d(TAG, response); + + + } catch (IOException e) { + Log.e(TAG, "Error: ", e); + } + + Message msg = mHandler.obtainMessage(); + msg.arg2 = STATE_PARSING; + mHandler.sendMessage(msg); + + /* + * { + "stat":"ok", + "list":[{ + "url":["http://www.worldcat.org/oclc/177669176?referer=xid"], + "publisher":"O'Reilly", + "form":["BA"], + "lccn":["2004273129"], + "lang":"eng", + "city":"Sebastopol, CA", + "author":"by Mark Lutz and David Ascher.", + "ed":"2nd ed.", + "year":"2003", + "isbn":["0596002815"], + "title":"Learning Python", + "oclcnum":["177669176", +.. + "748093898"]}]} + */ + + // This is OCLC-specific logic + try { + JSONObject result = new JSONObject(response); + + if (!result.getString("stat").equals("ok")) { + Log.e(TAG, "Error response received"); + msg = mHandler.obtainMessage(); + msg.arg2 = STATE_ERROR; + mHandler.sendMessage(msg); + return; + } + + result = result.getJSONArray("list").getJSONObject(0); + String form = result.getJSONArray("form").getString(0); + String type; + + if ("AA".equals(form)) type = "audioRecording"; + else if ("VA".equals(form)) type = "videoRecording"; + else if ("FA".equals(form)) type = "film"; + else type = "book"; + + // TODO Fix this + type = "book"; + + Item item = new Item(getActivity().getBaseContext(), type); + + JSONObject content = item.getContent(); + + if (result.has("lccn")) { + String lccn = "LCCN: " + result.getJSONArray("lccn").getString(0); + content.put("extra", lccn); + } + + if (result.has("isbn")) { + content.put("ISBN", result.getJSONArray("isbn").getString(0)); + } + + content.put("title", result.optString("title", "")); + content.put("place", result.optString("city", "")); + content.put("edition", result.optString("ed", "")); + content.put("language", result.optString("lang", "")); + content.put("publisher", result.optString("publisher", "")); + content.put("date", result.optString("year", "")); + + item.setTitle(result.optString("title", "")); + item.setYear(result.optString("year", "")); + + String author = result.optString("author", ""); + + item.setCreatorSummary(author); + JSONArray array = new JSONArray(); + JSONObject member = new JSONObject(); + member.accumulate("creatorType", "author"); + member.accumulate("name", author); + array.put(member); + content.put("creators", array); + + item.setContent(content); // NOTE: tags item as DIRTY + item.save(db); + + msg = mHandler.obtainMessage(); + Bundle data = new Bundle(); + data.putString("itemKey", item.getKey()); + msg.setData(data); + msg.arg2 = STATE_DONE; + mHandler.sendMessage(msg); + return; + } catch (JSONException e) { + Log.e(TAG, "exception parsing response", e); + msg = mHandler.obtainMessage(); + msg.arg2 = STATE_ERROR; + mHandler.sendMessage(msg); + return; + } + } + + public void setState(int state) { + mState = state; + } + } + + public void sync() { + // Check log-in -- prompt if not logged in + if (!ServerCredentials.check(getActivity().getBaseContext())) { + Toast.makeText(getActivity().getBaseContext(), getResources().getString(R.string.sync_log_in_first),Toast.LENGTH_SHORT).show(); + return; + } + + // Get credentials + ServerCredentials cred = new ServerCredentials(getActivity().getBaseContext()); + + // OLD COMMENT: Make this a collection-specific sync, preceding by de-dirtying + + Item.buildDirtyQueue(db); + ArrayList list = new ArrayList(); + + for (Item i : Item.dirtyQueue) { + Log.d(TAG, "Adding dirty item to sync: " + i.getTitle()); + list.add(cred.prep(APIRequest.update(i))); + } + + Log.d(TAG, "Adding sync request for all items"); + APIRequest req = APIRequest.fetchItems(false, cred); + req.setHandler(mEvent); + list.add(req); + + APIRequest[] templ = {}; // to ensure toArray converts to APIRequests, not Objects(?) + APIRequest[] reqs = list.toArray(templ); + + ZoteroAPITask task = new ZoteroAPITask(getActivity().getBaseContext()); + task.setHandler(syncHandler); + task.execute(reqs); + //Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_started),Toast.LENGTH_SHORT).show(); + } + + public void scanBarcode(Fragment_Items current) { + // If we're about to download from Google play, cancel that dialog + // and prompt from Amazon if we're on an Amazon device + FragmentIntentIntegrator integrator = new FragmentIntentIntegrator(this); + AlertDialog producedDialog = integrator.initiateScan(); + if (producedDialog != null && "amazon".equals(BuildConfig.FLAVOR)) { + producedDialog.dismiss(); + AmazonZxingGlue.showDownloadDialog(current.getActivity()); + } + } + + private final class FragmentIntentIntegrator extends IntentIntegrator { + + private final Fragment fragment; + + public FragmentIntentIntegrator(Fragment fragment) { + super(fragment.getActivity()); + this.fragment = fragment; + } + + @Override + protected void startActivityForResult(Intent intent, int code) { + fragment.startActivityForResult(intent, code); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mattrobertson/zotable/app/Fragment_Tags.java b/src/main/java/com/mattrobertson/zotable/app/Fragment_Tags.java new file mode 100755 index 0000000..dd18993 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/Fragment_Tags.java @@ -0,0 +1,645 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Message; +import android.support.v4.app.Fragment; +import android.support.v4.app.ListFragment; +import android.support.v4.widget.SwipeRefreshLayout; +import android.text.Editable; +import android.text.InputType; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import com.getbase.floatingactionbutton.FloatingActionButton; +import com.google.zxing.integration.android.IntentIntegrator; +import com.google.zxing.integration.android.IntentResult; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemCollection; +import com.mattrobertson.zotable.app.data.TagAdapter; +import com.mattrobertson.zotable.app.task.APIRequest; +import com.nononsenseapps.filepicker.FilePickerActivity; + +import org.apache.http.util.ByteArrayBuffer; +import org.jetbrains.annotations.Nullable; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; + +public class Fragment_Tags extends ListFragment { + + private static final String TAG = "Fragment_Tags"; + + static final int DIALOG_NEW = 1; + static final int DIALOG_SORT = 2; + static final int DIALOG_IDENTIFIER = 3; + static final int DIALOG_PROGRESS = 6; + + static final int FILE_CODE = 21; + + Bundle b = new Bundle(); + + /** + * Allowed sort orderings + */ + static final String[] SORTS = { + "tag COLLATE NOCASE", + "COUNT(*) DESC" + }; + + /** + * Strings providing the names of each ordering, respectively + */ + static final int[] SORT_NAMES = { + R.string.sort_tag_name, + R.string.sort_item_count + }; + private static final String SORT_CHOICE_TAGS = "sort_choice_tags"; + + private ProgressDialog mProgressDialog; + private ProgressThread progressThread; + + public String sortBy = "tag COLLATE NOCASE"; + + private Database db; + + /** + * Refreshes the current list adapter + */ + private void refreshView() { + TagAdapter adapter = (TagAdapter) getListAdapter(); + Cursor newCursor = create(); + adapter.changeCursor(newCursor); + adapter.notifyDataSetChanged(); + Log.d(TAG, "refreshing view on request"); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_tags, container, false); + } + + /** Called when the activity is first created. */ + @Override + public void onStart() { + super.onStart(); + + setHasOptionsMenu(true); + + String persistedSort = Persistence.read(SORT_CHOICE_TAGS); + if (persistedSort != null) sortBy = persistedSort; + + db = new Database(getActivity()); + + TagAdapter TagAdapter = new TagAdapter(getActivity(), create()); + setListAdapter(TagAdapter); + + ListView lv = getListView(); + + lv.setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView parent, View view, int position, long id) { + + TagAdapter adapter = (TagAdapter) parent.getAdapter(); + Cursor cur = adapter.getCursor(); + // Place the cursor at the selected item + if (cur.moveToPosition(position)) { + String tagName = cur.getString(2); + + Intent i = new Intent(getActivity().getBaseContext(), TagItemsActivity.class); + i.putExtra("com.mattrobertson.zotable.app.tagName", tagName); + startActivity(i); + } else { + // failed to move cursor-- show a toast + TextView tvTag = (TextView) view.findViewById(R.id.tag_name); + Toast.makeText(getActivity().getApplicationContext(), "Can't view tag " + tvTag.getText(), Toast.LENGTH_SHORT).show(); + return; + } + return; + } + }); + + // Create floating action buttons + FloatingActionButton fabScan = (FloatingActionButton)getActivity().findViewById(R.id.btnFabScan); + FloatingActionButton fabIsbn = (FloatingActionButton)getActivity().findViewById(R.id.btnFabIsbn); + FloatingActionButton fabUpload = (FloatingActionButton)getActivity().findViewById(R.id.btnFabUpload); + FloatingActionButton fabManual = (FloatingActionButton)getActivity().findViewById(R.id.btnFabManual); + + fabScan.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + scanBarcode(Fragment_Tags.this); + } + }); + + fabIsbn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showDialog(DIALOG_IDENTIFIER); + } + }); + + fabUpload.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // This always works + Intent i = new Intent(getActivity(), FilePickerActivity.class); + // This works if you defined the intent filter + // Intent i = new Intent(Intent.ACTION_GET_CONTENT); + + // Set these depending on your use case. These are the defaults. + i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false); + i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); + i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE); + + // Configure initial directory by specifying a String. + // You could specify a String like "/storage/emulated/0/", but that can + // dangerous. Always use Android's API calls to get paths to the SD-card or + // internal memory. + String filepath = Environment.getExternalStorageDirectory().getPath(); + if (new File("/sdcard/").exists()) + filepath = "/sdcard/"; // prefer /sdcard... implemented this way in case DNE + + i.putExtra(FilePickerActivity.EXTRA_START_PATH, filepath); + + startActivityForResult(i, FILE_CODE); + } + }); + + fabManual.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showDialog(DIALOG_NEW); + } + }); + } + + public void onResume() { + TagAdapter adapter = (TagAdapter) getListAdapter(); + // XXX This may be too agressive-- fix if causes issues + Cursor newCursor = create(); + adapter.changeCursor(newCursor); + adapter.notifyDataSetChanged(); + if (db == null) db = new Database(getActivity()); + super.onResume(); + } + + public void onDestroy() { + TagAdapter adapter = (TagAdapter) getListAdapter(); + + if (adapter != null) { + Cursor cur = adapter.getCursor(); + + if (cur != null) + cur.close(); + } + + if (db != null) + db.close(); + + super.onDestroy(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + + menu.clear(); + + inflater.inflate(R.menu.zotero_menu,menu); + + // Turn on sort item + MenuItem sort = menu.findItem(R.id.do_sort); + sort.setEnabled(true); + sort.setVisible(true); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + switch (item.getItemId()) { + case R.id.do_sort: + showDialog(DIALOG_SORT); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + public void showDialog(int id) { + switch (id) { + case DIALOG_NEW: + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(getResources().getString(R.string.item_type)) + // XXX i18n + .setItems(Item.ITEM_TYPES_EN, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int pos) { + Item item = new Item(getActivity().getBaseContext(), Item.ITEM_TYPES[pos]); + item.dirty = APIRequest.API_DIRTY; + item.save(db); + + Log.d(TAG, "Loading item data with key: " + item.getKey()); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getActivity().getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); + startActivity(i); + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + return; + case DIALOG_SORT: + + // We generate the sort name list for our current locale + String[] sorts = new String[SORT_NAMES.length]; + for (int j = 0; j < SORT_NAMES.length; j++) { + sorts[j] = getResources().getString(SORT_NAMES[j]); + } + + AlertDialog.Builder builder2 = new AlertDialog.Builder(getActivity()); + builder2.setTitle(getResources().getString(R.string.set_sort_order)) + .setItems(sorts, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int pos) { + Cursor cursor; + setSortBy(SORTS[pos]); + ItemCollection collection; + + cursor = create(); + + TagAdapter adapter = (TagAdapter) getListAdapter(); + adapter.changeCursor(cursor); + Log.d(TAG, "Re-sorting by: " + SORTS[pos]); + + Persistence.write(SORT_CHOICE_TAGS, SORTS[pos]); + } + }); + AlertDialog dialog2 = builder2.create(); + dialog2.show(); + return; + case DIALOG_PROGRESS: + mProgressDialog = new ProgressDialog(getActivity()); + mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + mProgressDialog.setIndeterminate(true); + mProgressDialog.setMessage(getResources().getString(R.string.identifier_looking_up)); + mProgressDialog.show(); + return; + case DIALOG_IDENTIFIER: + final EditText input = new EditText(getActivity()); + input.setHint(getResources().getString(R.string.identifier_hint)); + input.setInputType(InputType.TYPE_CLASS_NUMBER); + + final Fragment_Tags current = this; + + dialog = new AlertDialog.Builder(getActivity()) + .setTitle(getResources().getString(R.string.identifier_message)) + .setView(input) + .setPositiveButton(getResources().getString(R.string.menu_search), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + Editable value = input.getText(); + // run search + Bundle c = new Bundle(); + c.putString("mode", "isbn"); + c.putString("identifier", value.toString()); + + // TODO: quick & dirty fix... check here if error occurs + Fragment_Tags.this.b = c; + + Log.d(TAG, b.getString("identifier")); + progressThread = new ProgressThread(handler, b); + progressThread.start(); + + showDialog(DIALOG_PROGRESS); + } + }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // do nothing + } + }).create(); + dialog.show(); + return; + } + } + + /* Sorting */ + public void setSortBy(String sort) { + this.sortBy = sort; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + Log.d(TAG, "_____________________on_activity_result"); + + IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); + if (scanResult != null) { + // handle scan result + Bundle b = new Bundle(); + b.putString("mode", "isbn"); + b.putString("identifier", scanResult.getContents()); + + if (scanResult != null && scanResult.getContents() != null) { + Log.d(TAG, b.getString("identifier")); + progressThread = new ProgressThread(handler, b); + progressThread.start(); + showDialog(DIALOG_PROGRESS); + } else { + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_scan_failed), + Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_scan_failed), + Toast.LENGTH_SHORT).show(); + } + } + + final Handler handler = new Handler() { + public void handleMessage(Message msg) { + Log.d(TAG, "______________________handle_message"); + if (ProgressThread.STATE_DONE == msg.arg2) { + Bundle data = msg.getData(); + String itemKey = data.getString("itemKey"); + if (itemKey != null) { + + mProgressDialog.dismiss(); + mProgressDialog = null; + + Log.d(TAG, "Loading new item data with key: " + itemKey); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getActivity().getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", itemKey); + startActivity(i); + } + return; + } + + if (ProgressThread.STATE_PARSING == msg.arg2) { + if (mProgressDialog != null) + mProgressDialog.setMessage(getResources().getString(R.string.identifier_processing)); + return; + } + + if (ProgressThread.STATE_ERROR == msg.arg2) { + getActivity().dismissDialog(DIALOG_PROGRESS); + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_lookup_failed), + Toast.LENGTH_SHORT).show(); + progressThread.setState(ProgressThread.STATE_DONE); + return; + } + } + }; + + private class ProgressThread extends Thread { + Handler mHandler; + Bundle arguments; + final static int STATE_DONE = 5; + final static int STATE_FETCHING = 1; + final static int STATE_PARSING = 6; + final static int STATE_ERROR = 7; + + int mState; + + ProgressThread(Handler h, Bundle b) { + mHandler = h; + arguments = b; + Log.d(TAG, "_____________________thread_constructor"); + } + + public void run() { + Log.d(TAG, "_____________________thread_run"); + mState = STATE_FETCHING; + + // Setup + String identifier = arguments.getString("identifier"); + String mode = arguments.getString("mode"); + URL url; + String urlstring; + + String response = ""; + + if ("isbn".equals(mode)) { + urlstring = "http://xisbn.worldcat.org/webservices/xid/isbn/" + + identifier + + "?method=getMetadata&fl=*&format=json&count=1"; + } else { + urlstring = ""; + } + + try { + Log.d(TAG, "Fetching from: " + urlstring); + url = new URL(urlstring); + + /* Open a connection to that URL. */ + URLConnection ucon = url.openConnection(); + /* + * Define InputStreams to read from the URLConnection. + */ + InputStream is = ucon.getInputStream(); + BufferedInputStream bis = new BufferedInputStream(is, 16000); + + ByteArrayBuffer baf = new ByteArrayBuffer(50); + int current = 0; + + /* + * Read bytes to the Buffer until there is nothing more to read(-1). + */ + while (mState == STATE_FETCHING + && (current = bis.read()) != -1) { + baf.append((byte) current); + } + response = new String(baf.toByteArray()); + Log.d(TAG, response); + + + } catch (IOException e) { + Log.e(TAG, "Error: ", e); + } + + Message msg = mHandler.obtainMessage(); + msg.arg2 = STATE_PARSING; + mHandler.sendMessage(msg); + + /* + * { + "stat":"ok", + "list":[{ + "url":["http://www.worldcat.org/oclc/177669176?referer=xid"], + "publisher":"O'Reilly", + "form":["BA"], + "lccn":["2004273129"], + "lang":"eng", + "city":"Sebastopol, CA", + "author":"by Mark Lutz and David Ascher.", + "ed":"2nd ed.", + "year":"2003", + "isbn":["0596002815"], + "title":"Learning Python", + "oclcnum":["177669176", +.. + "748093898"]}]} + */ + + // This is OCLC-specific logic + try { + JSONObject result = new JSONObject(response); + + if (!result.getString("stat").equals("ok")) { + Log.e(TAG, "Error response received"); + msg = mHandler.obtainMessage(); + msg.arg2 = STATE_ERROR; + mHandler.sendMessage(msg); + return; + } + + result = result.getJSONArray("list").getJSONObject(0); + String form = result.getJSONArray("form").getString(0); + String type; + + if ("AA".equals(form)) type = "audioRecording"; + else if ("VA".equals(form)) type = "videoRecording"; + else if ("FA".equals(form)) type = "film"; + else type = "book"; + + // TODO Fix this + type = "book"; + + Item item = new Item(getActivity().getBaseContext(), type); + + JSONObject content = item.getContent(); + + if (result.has("lccn")) { + String lccn = "LCCN: " + result.getJSONArray("lccn").getString(0); + content.put("extra", lccn); + } + + if (result.has("isbn")) { + content.put("ISBN", result.getJSONArray("isbn").getString(0)); + } + + content.put("title", result.optString("title", "")); + content.put("place", result.optString("city", "")); + content.put("edition", result.optString("ed", "")); + content.put("language", result.optString("lang", "")); + content.put("publisher", result.optString("publisher", "")); + content.put("date", result.optString("year", "")); + + item.setTitle(result.optString("title", "")); + item.setYear(result.optString("year", "")); + + String author = result.optString("author", ""); + + item.setCreatorSummary(author); + JSONArray array = new JSONArray(); + JSONObject member = new JSONObject(); + member.accumulate("creatorType", "author"); + member.accumulate("name", author); + array.put(member); + content.put("creators", array); + + item.setContent(content); // NOTE: tags item as DIRTY + item.save(db); + + msg = mHandler.obtainMessage(); + Bundle data = new Bundle(); + data.putString("itemKey", item.getKey()); + msg.setData(data); + msg.arg2 = STATE_DONE; + mHandler.sendMessage(msg); + return; + } catch (JSONException e) { + Log.e(TAG, "exception parsing response", e); + msg = mHandler.obtainMessage(); + msg.arg2 = STATE_ERROR; + mHandler.sendMessage(msg); + return; + } + } + + public void setState(int state) { + mState = state; + } + } + + /** + * Gives a cursor containing all tags + * @return + */ + public Cursor create() { + //Cursor cursor = db.rawQuery("SELECT * FROM tags GROUP BY tag ORDER BY tag COLLATE NOCASE", null); + //Cursor cursor = db.rawQuery("SELECT * FROM tags GROUP BY tag ORDER BY COUNT(*) DESC", null); + Cursor cursor = db.rawQuery("SELECT * FROM tags GROUP BY tag ORDER BY " + sortBy, null); + + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + + return cursor; + } + + public void scanBarcode(Fragment_Tags current) { + // If we're about to download from Google play, cancel that dialog + // and prompt from Amazon if we're on an Amazon device + FragmentIntentIntegrator integrator = new FragmentIntentIntegrator(this); + @Nullable AlertDialog producedDialog = integrator.initiateScan(); + if (producedDialog != null && "amazon".equals(BuildConfig.FLAVOR)) { + producedDialog.dismiss(); + AmazonZxingGlue.showDownloadDialog(current.getActivity()); + } + } + + private final class FragmentIntentIntegrator extends IntentIntegrator { + + private final Fragment fragment; + + public FragmentIntentIntegrator(Fragment fragment) { + super(fragment.getActivity()); + this.fragment = fragment; + } + + @Override + protected void startActivityForResult(Intent intent, int code) { + fragment.startActivityForResult(intent, code); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/gimranov/zandy/app/ItemActivity.java b/src/main/java/com/mattrobertson/zotable/app/ItemActivity.java old mode 100644 new mode 100755 similarity index 74% rename from src/main/java/com/gimranov/zandy/app/ItemActivity.java rename to src/main/java/com/mattrobertson/zotable/app/ItemActivity.java index 0285120..566336c --- a/src/main/java/com/gimranov/zandy/app/ItemActivity.java +++ b/src/main/java/com/mattrobertson/zotable/app/ItemActivity.java @@ -1,54 +1,65 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Zandy is distributed in the hope that it will be useful, + * Zotable is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app; +package com.mattrobertson.zotable.app; +import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.ListActivity; import android.app.ProgressDialog; import android.app.SearchManager; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; +import android.net.Uri; import android.os.Bundle; +import android.os.Environment; import android.os.Handler; import android.os.Message; +import android.support.v4.widget.SwipeRefreshLayout; import android.text.Editable; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; +import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.EditText; import android.widget.ListView; +import android.widget.SearchView; import android.widget.TextView; import android.widget.Toast; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.data.Item; -import com.gimranov.zandy.app.data.ItemAdapter; -import com.gimranov.zandy.app.data.ItemCollection; -import com.gimranov.zandy.app.task.APIEvent; -import com.gimranov.zandy.app.task.APIRequest; -import com.gimranov.zandy.app.task.ZoteroAPITask; +import com.getbase.floatingactionbutton.FloatingActionButton; + + +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemAdapter; +import com.mattrobertson.zotable.app.data.ItemCollection; +import com.mattrobertson.zotable.app.task.APIEvent; +import com.mattrobertson.zotable.app.task.APIRequest; +import com.mattrobertson.zotable.app.task.ZoteroAPITask; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; +import com.nononsenseapps.filepicker.FilePickerActivity; import com.squareup.otto.Subscribe; import org.apache.http.util.ByteArrayBuffer; @@ -58,22 +69,24 @@ import org.json.JSONObject; import java.io.BufferedInputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; -public class ItemActivity extends ListActivity { +public class ItemActivity extends ListActivity implements SwipeRefreshLayout.OnRefreshListener { - private static final String TAG = "com.gimranov.zandy.app.ItemActivity"; + private static final String TAG = "ItemActivity"; - static final int DIALOG_VIEW = 0; static final int DIALOG_NEW = 1; static final int DIALOG_SORT = 2; static final int DIALOG_IDENTIFIER = 3; static final int DIALOG_PROGRESS = 6; + static final int FILE_CODE = 21; + /** * Allowed sort orderings */ @@ -99,6 +112,8 @@ public class ItemActivity extends ListActivity { private String query; private Database db; + private SwipeRefreshLayout swipeLayout; + private ProgressDialog mProgressDialog; private ProgressThread progressThread; @@ -124,9 +139,10 @@ public void handleMessage(Message msg) { if (msg.arg1 == APIRequest.BATCH_DONE) { Application.getInstance().getBus().post(SyncEvent.COMPLETE); - Toast.makeText(getApplicationContext(), - getResources().getString(R.string.sync_complete), - Toast.LENGTH_SHORT).show(); + // Sync success w/o erros -- set all items to clean status in DB + Item.setAllClean(db); + + //Toast.makeText(getApplicationContext(),getResources().getString(R.string.sync_complete),Toast.LENGTH_SHORT).show(); return; } @@ -209,8 +225,15 @@ public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.items); + swipeLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_container); + swipeLayout.setOnRefreshListener(this); + swipeLayout.setColorScheme(R.color.light_blue, + R.color.blue, + R.color.dark_blue, + R.color.darkest_blue); + Intent intent = getIntent(); - collectionKey = intent.getStringExtra("com.gimranov.zandy.app.collectionKey"); + collectionKey = intent.getStringExtra("com.mattrobertson.zotable.app.collectionKey"); ItemCollection coll = ItemCollection.load(collectionKey, db); APIRequest req; @@ -228,7 +251,7 @@ public void onCreate(Bundle savedInstanceState) { ItemAdapter adapter = (ItemAdapter) getListAdapter(); Cursor cur = adapter.getCursor(); - if (intent.getBooleanExtra("com.gimranov.zandy.app.rerequest", false) + if (intent.getBooleanExtra("com.mattrobertson.zotable.app.rerequest", false) || cur == null || cur.getCount() == 0) { @@ -238,17 +261,14 @@ public void onCreate(Bundle savedInstanceState) { return; } - Toast.makeText(this, - getResources().getString(R.string.collection_empty), - Toast.LENGTH_SHORT).show(); + Toast.makeText(this,getResources().getString(R.string.collection_empty),Toast.LENGTH_SHORT).show(); Log.d(TAG, "Running a request to populate missing items"); ZoteroAPITask task = new ZoteroAPITask(this); req.setHandler(mEvent); task.execute(req); - } - ListView lv = getListView(); + final ListView lv = getListView(); lv.setOnItemClickListener(new OnItemClickListener() { public void onItemClick(AdapterView parent, View view, int position, long id) { // If we have a click on an item, do something... @@ -262,8 +282,8 @@ public void onItemClick(AdapterView parent, View view, int position, long id) Log.d(TAG, "Loading item data with key: " + item.getKey()); // We create and issue a specified intent with the necessary data Intent i = new Intent(getBaseContext(), ItemDataActivity.class); - i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); - i.putExtra("com.gimranov.zandy.app.itemDbId", item.dbId); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); + i.putExtra("com.mattrobertson.zotable.app.itemDbId", item.dbId); startActivity(i); } else { // failed to move cursor-- show a toast @@ -274,6 +294,74 @@ public void onItemClick(AdapterView parent, View view, int position, long id) } } }); + + lv.setOnScrollListener(new AbsListView.OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView absListView, int i) {} + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + int topRowVerticalPosition = (lv == null || lv.getChildCount() == 0) ? 0 : lv.getChildAt(0).getTop(); + + swipeLayout.setEnabled(firstVisibleItem == 0 && topRowVerticalPosition >= 0); + } + }); + + // Create floating action buttons + FloatingActionButton fabScan = (FloatingActionButton)findViewById(R.id.btnFabScan); + FloatingActionButton fabIsbn = (FloatingActionButton)findViewById(R.id.btnFabIsbn); + FloatingActionButton fabUpload = (FloatingActionButton)findViewById(R.id.btnFabUpload); + FloatingActionButton fabManual = (FloatingActionButton)findViewById(R.id.btnFabManual); + + fabScan.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + scanBarcode(ItemActivity.this); + } + }); + + fabIsbn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + removeDialog(DIALOG_IDENTIFIER); + showDialog(DIALOG_IDENTIFIER); + } + }); + + fabManual.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + removeDialog(DIALOG_NEW); + showDialog(DIALOG_NEW); + } + }); + + fabUpload.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // This always works + Intent i = new Intent(ItemActivity.this, FilePickerActivity.class); + // This works if you defined the intent filter + // Intent i = new Intent(Intent.ACTION_GET_CONTENT); + + // Set these depending on your use case. These are the defaults. + i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false); + i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); + i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE); + + // Configure initial directory by specifying a String. + // You could specify a String like "/storage/emulated/0/", but that can + // dangerous. Always use Android's API calls to get paths to the SD-card or + // internal memory. + String filepath = Environment.getExternalStorageDirectory().getPath(); + if (new File("/sdcard/").exists()) + filepath = "/sdcard/"; // prefer /sdcard... implemented this way in case DNE + + i.putExtra(FilePickerActivity.EXTRA_START_PATH, filepath); + + startActivityForResult(i, FILE_CODE); + } + }); } @Override @@ -298,6 +386,16 @@ protected void onPause() { Application.getInstance().getBus().unregister(this); } + @Override + public void onRefresh() { + new Handler().postDelayed(new Runnable() { + @Override public void run() { + swipeLayout.setRefreshing(false); + sync(); + } + }, 5000); + } + private void prepareAdapter() { ItemAdapter adapter = new ItemAdapter(this, prepareCursor()); setListAdapter(adapter); @@ -315,14 +413,14 @@ private Cursor prepareCursor() { } else if (query != null) { cursor = getCursor(query); this.setTitle(getResources().getString(R.string.search_results, query)); - } else if (intent.getStringExtra("com.gimranov.zandy.app.tag") != null) { - String tag = intent.getStringExtra("com.gimranov.zandy.app.tag"); + } else if (intent.getStringExtra("com.mattrobertson.zotable.app.tag") != null) { + String tag = intent.getStringExtra("com.mattrobertson.zotable.app.tag"); Query q = new Query(); q.set("tag", tag); cursor = getCursor(q); this.setTitle(getResources().getString(R.string.tag_viewing_items, tag)); } else { - collectionKey = intent.getStringExtra("com.gimranov.zandy.app.collectionKey"); + collectionKey = intent.getStringExtra("com.mattrobertson.zotable.app.collectionKey"); ItemCollection coll; @@ -359,7 +457,7 @@ public void onClick(DialogInterface dialog, int pos) { Log.d(TAG, "Loading item data with key: " + item.getKey()); // We create and issue a specified intent with the necessary data Intent i = new Intent(getBaseContext(), ItemDataActivity.class); - i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); startActivity(i); } }); @@ -425,17 +523,6 @@ public void onClick(DialogInterface dialog, int whichButton) { ItemActivity.this.b = c; showDialog(DIALOG_PROGRESS); } - }).setNeutralButton(getResources().getString(R.string.scan), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - // If we're about to download from Google play, cancel that dialog - // and prompt from Amazon if we're on an Amazon device - IntentIntegrator integrator = new IntentIntegrator(current); - @Nullable AlertDialog producedDialog = integrator.initiateScan(); - if (producedDialog != null && "amazon".equals(BuildConfig.FLAVOR)) { - producedDialog.dismiss(); - AmazonZxingGlue.showDownloadDialog(current); - } - } }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { // do nothing @@ -469,10 +556,9 @@ public boolean onCreateOptionsMenu(Menu menu) { search.setEnabled(true); search.setVisible(true); - // Turn on identifier item - MenuItem identifier = menu.findItem(R.id.do_identifier); - identifier.setEnabled(true); - identifier.setVisible(true); + SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); + SearchView searchView = (SearchView) menu.findItem(R.id.do_search).getActionView(); + searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); return true; } @@ -481,54 +567,8 @@ public boolean onCreateOptionsMenu(Menu menu) { public boolean onOptionsItemSelected(MenuItem item) { // Handle item selection switch (item.getItemId()) { - case R.id.do_sync: - if (!ServerCredentials.check(getBaseContext())) { - Toast.makeText(getBaseContext(), getResources().getString(R.string.sync_log_in_first), - Toast.LENGTH_SHORT).show(); - return true; - } - - // Get credentials - ServerCredentials cred = new ServerCredentials(getBaseContext()); - - // Make this a collection-specific sync, preceding by de-dirtying - Item.queue(db); - ArrayList list = new ArrayList(); - APIRequest[] templ = {}; - for (Item i : Item.queue) { - Log.d(TAG, "Adding dirty item to sync: " + i.getTitle()); - list.add(cred.prep(APIRequest.update(i))); - } - - if (collectionKey == null) { - Log.d(TAG, "Adding sync request for all items"); - APIRequest req = APIRequest.fetchItems(false, cred); - req.setHandler(mEvent); - list.add(req); - } else { - Log.d(TAG, "Adding sync request for collection: " + collectionKey); - APIRequest req = APIRequest.fetchItems(collectionKey, true, cred); - req.setHandler(mEvent); - list.add(req); - } - APIRequest[] reqs = list.toArray(templ); - - ZoteroAPITask task = new ZoteroAPITask(getBaseContext()); - task.setHandler(syncHandler); - task.execute(reqs); - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_started), - Toast.LENGTH_SHORT).show(); - return true; - case R.id.do_new: - removeDialog(DIALOG_NEW); - showDialog(DIALOG_NEW); - return true; - case R.id.do_identifier: - removeDialog(DIALOG_IDENTIFIER); - showDialog(DIALOG_IDENTIFIER); - return true; case R.id.do_search: - onSearchRequested(); + //onSearchRequested(); return true; case R.id.do_prefs: Intent i = new Intent(getBaseContext(), SettingsActivity.class); @@ -597,29 +637,31 @@ public void syncComplete(SyncEvent event) { public void onActivityResult(int requestCode, int resultCode, Intent intent) { Log.d(TAG, "_____________________on_activity_result"); - IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); - if (scanResult != null) { - // handle scan result - Bundle b = new Bundle(); - b.putString("mode", "isbn"); - b.putString("identifier", scanResult.getContents()); - if (scanResult != null - && scanResult.getContents() != null) { - Log.d(TAG, b.getString("identifier")); - progressThread = new ProgressThread(handler, b); - progressThread.start(); - this.b = b; - removeDialog(DIALOG_PROGRESS); - showDialog(DIALOG_PROGRESS); + if (requestCode == FILE_CODE) { + if (resultCode == Activity.RESULT_OK) { + Uri uri = intent.getData(); + Util.handleUpload(uri.getPath(), this, db, false); + } + } + else { + IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); + if (scanResult != null) { + // handle scan result + Bundle b = new Bundle(); + b.putString("mode", "isbn"); + b.putString("identifier", scanResult.getContents()); + + if (scanResult != null && scanResult.getContents() != null) { + Log.d(TAG, b.getString("identifier")); + progressThread = new ProgressThread(handler, b); + progressThread.start(); + showDialog(DIALOG_PROGRESS); + } else { + Toast.makeText(this,getResources().getString(R.string.identifier_scan_failed),Toast.LENGTH_SHORT).show(); + } } else { - Toast.makeText(getApplicationContext(), - getResources().getString(R.string.identifier_scan_failed), - Toast.LENGTH_SHORT).show(); + Toast.makeText(this,getResources().getString(R.string.identifier_scan_failed),Toast.LENGTH_SHORT).show(); } - } else { - Toast.makeText(getApplicationContext(), - getResources().getString(R.string.identifier_scan_failed), - Toast.LENGTH_SHORT).show(); } } @@ -643,7 +685,7 @@ public void handleMessage(Message msg) { Log.d(TAG, "Loading new item data with key: " + itemKey); // We create and issue a specified intent with the necessary data Intent i = new Intent(getBaseContext(), ItemDataActivity.class); - i.putExtra("com.gimranov.zandy.app.itemKey", itemKey); + i.putExtra("com.mattrobertson.zotable.app.itemKey", itemKey); startActivity(i); } return; @@ -835,4 +877,52 @@ public void setState(int state) { mState = state; } } + + public void sync() { + if (!ServerCredentials.check(getBaseContext())) { + Toast.makeText(getBaseContext(), getResources().getString(R.string.sync_log_in_first),Toast.LENGTH_SHORT).show(); + return; + } + + // Get credentials + ServerCredentials cred = new ServerCredentials(getBaseContext()); + + // Make this a collection-specific sync, preceding by de-dirtying + Item.buildDirtyQueue(db); + ArrayList list = new ArrayList(); + APIRequest[] templ = {}; + for (Item i : Item.dirtyQueue) { + Log.d(TAG, "Adding dirty item to sync: " + i.getTitle()); + list.add(cred.prep(APIRequest.update(i))); + } + + if (collectionKey == null) { + Log.d(TAG, "Adding sync request for all items"); + APIRequest req = APIRequest.fetchItems(false, cred); + req.setHandler(mEvent); + list.add(req); + } else { + Log.d(TAG, "Adding sync request for collection: " + collectionKey); + APIRequest req = APIRequest.fetchItems(collectionKey, true, cred); + req.setHandler(mEvent); + list.add(req); + } + APIRequest[] reqs = list.toArray(templ); + + ZoteroAPITask task = new ZoteroAPITask(getBaseContext()); + task.setHandler(syncHandler); + task.execute(reqs); + //Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_started),Toast.LENGTH_SHORT).show(); + } + + public void scanBarcode(ItemActivity current) { + // If we're about to download from Google play, cancel that dialog + // and prompt from Amazon if we're on an Amazon device + IntentIntegrator integrator = new IntentIntegrator(current); + @Nullable AlertDialog producedDialog = integrator.initiateScan(); + if (producedDialog != null && "amazon".equals(BuildConfig.FLAVOR)) { + producedDialog.dismiss(); + AmazonZxingGlue.showDownloadDialog(current); + } + } } diff --git a/src/main/java/com/gimranov/zandy/app/ItemDataActivity.java b/src/main/java/com/mattrobertson/zotable/app/ItemDataActivity.java old mode 100644 new mode 100755 similarity index 74% rename from src/main/java/com/gimranov/zandy/app/ItemDataActivity.java rename to src/main/java/com/mattrobertson/zotable/app/ItemDataActivity.java index 7872503..7b5b199 --- a/src/main/java/com/gimranov/zandy/app/ItemDataActivity.java +++ b/src/main/java/com/mattrobertson/zotable/app/ItemDataActivity.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Zandy is distributed in the hope that it will be useful, + * Zotable is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app; +package com.mattrobertson.zotable.app; import java.util.ArrayList; @@ -31,42 +31,45 @@ import android.text.InputType; import android.text.TextUtils; import android.util.Log; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; -import android.view.View.OnCreateContextMenuListener; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.BaseExpandableListAdapter; import android.widget.EditText; import android.widget.ExpandableListView; +import android.widget.ImageView; import android.widget.TextView; import android.widget.TextView.BufferType; import android.widget.Toast; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.data.Item; -import com.gimranov.zandy.app.task.APIRequest; -import com.gimranov.zandy.app.task.ZoteroAPITask; + +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; public class ItemDataActivity extends ExpandableListActivity { - private static final String TAG = "com.gimranov.zandy.app.ItemDataActivity"; + private static final String TAG = "ItemDataActivity"; static final int DIALOG_SINGLE_VALUE = 0; static final int DIALOG_ITEM_TYPE = 1; static final int DIALOG_CONFIRM_NAVIGATE = 4; static final int DIALOG_CONFIRM_DELETE = 5; - + public Item item; private Database db; + BundleListAdapter mBundleListAdapter; + + String itemKey = ""; + + ArrayList rows; + /** * For Bundle passing to Dialogs in API <= 7 */ @@ -76,17 +79,19 @@ public class ItemDataActivity extends ExpandableListActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - + + getActionBar().setDisplayHomeAsUpEnabled(true); + db = new Database(this); /* Get the incoming data from the calling activity */ - String itemKey = getIntent().getStringExtra("com.gimranov.zandy.app.itemKey"); + itemKey = getIntent().getStringExtra("com.mattrobertson.zotable.app.itemKey"); item = Item.load(itemKey, db); // When an item in the view has been updated via a sync, the temporary key may have // been swapped out, so we fall back on the DB ID if (item == null) { - String itemDbId = getIntent().getStringExtra("com.gimranov.zandy.app.itemDbId"); + String itemDbId = getIntent().getStringExtra("com.mattrobertson.zotable.app.itemDbId"); if (itemDbId == null) { Log.d(TAG, "Failed to load item using itemKey and no dbId specified. Give up and finish activity."); finish(); @@ -101,20 +106,20 @@ public void onCreate(Bundle savedInstanceState) { else this.setTitle(getResources().getString(R.string.item_details)); - ArrayList rows = item.toBundleArray(db); + rows = item.toBundleArray(db); - BundleListAdapter mBundleListAdapter = new BundleListAdapter(); + mBundleListAdapter = new BundleListAdapter(); mBundleListAdapter.bundles = rows; setListAdapter(mBundleListAdapter); + registerForContextMenu(getExpandableListView()); - ExpandableListView lv = getExpandableListView(); + final ExpandableListView lv = getExpandableListView(); lv.setGroupIndicator(getResources().getDrawable(R.drawable.list_child_indicator)); lv.setTextFilterEnabled(true); lv.setOnChildClickListener(new ExpandableListView.OnChildClickListener() { @Override - public boolean onChildClick(ExpandableListView parent, View v, - int groupPosition, int childPosition, long id) { + public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { return true; } }); @@ -125,7 +130,57 @@ public boolean onGroupClick(ExpandableListView parent, View view, int position, // If we have a click on an entry, do something... BundleListAdapter adapter = (BundleListAdapter) parent.getExpandableListAdapter(); Bundle row = adapter.getGroup(position); - if (row.getString("label").equals("url")) { + if (row.getString("label").equals("title")) { + final EditText input = new EditText(ItemDataActivity.this); + input.setText(row.getString("content"), BufferType.EDITABLE); + + AlertDialog.Builder builder = new AlertDialog.Builder(ItemDataActivity.this); + builder.setTitle("Title") + .setView(input) + .setPositiveButton("Update", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + item.setTitle(input.getText().toString()); + item.save(db); + refreshData(); + } + }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // do nothing + } + }); + AlertDialog dialog = builder.show(); + + int textViewId = dialog.getContext().getResources().getIdentifier("android:id/alertTitle", null, null); + TextView tv = (TextView) dialog.findViewById(textViewId); + tv.setTextColor(getResources().getColor(R.color.white)); + } + else if (row.getString("label").equals("date")) { +/* + final EditText input = new EditText(ItemDataActivity.this); + input.setText(row.getString("content"), BufferType.EDITABLE); + + AlertDialog.Builder builder = new AlertDialog.Builder(ItemDataActivity.this); + builder.setTitle("Date") + .setView(input) + .setPositiveButton("Update", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + item.setDate(input.getText().toString()); + item.save(db); + lv.invalidate(); + } + }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // do nothing + } + }); + AlertDialog dialog = builder.show(); + + int textViewId = dialog.getContext().getResources().getIdentifier("android:id/alertTitle", null, null); + TextView tv = (TextView) dialog.findViewById(textViewId); + tv.setTextColor(getResources().getColor(R.color.white)); +*/ + } + else if (row.getString("label").equals("url")) { row.putString("url", row.getString("content")); removeDialog(DIALOG_CONFIRM_NAVIGATE); ItemDataActivity.this.b = row; @@ -141,40 +196,38 @@ public boolean onGroupClick(ExpandableListView parent, View view, int position, } else if (row.getString("label").equals("creators")) { Log.d(TAG, "Trying to start creators activity"); Intent i = new Intent(getBaseContext(), CreatorActivity.class); - i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); startActivity(i); return true; } else if (row.getString("label").equals("tags")) { Log.d(TAG, "Trying to start tag activity"); Intent i = new Intent(getBaseContext(), TagActivity.class); - i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); startActivity(i); return true; } else if (row.getString("label").equals("children")) { +/* Log.d(TAG, "Trying to start attachment activity"); Intent i = new Intent(getBaseContext(), AttachmentActivity.class); - i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); startActivity(i); return true; +*/ } else if (row.getString("label").equals("collections")) { Log.d(TAG, "Trying to start collection membership activity"); Intent i = new Intent(getBaseContext(), CollectionMembershipActivity.class); - i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); startActivity(i); return true; } - // Suppress toast if we're going to expand the view anyway - if (!"abstractNote".equals(row.getSerializable("label"))) { - Toast.makeText(getApplicationContext(), row.getString("content"), - Toast.LENGTH_SHORT).show(); - } + return false; } }); /* * On long click, we bring up an edit dialog. - */ + * lv.setOnCreateContextMenuListener(new OnCreateContextMenuListener() { @Override public void onCreateContextMenu(ContextMenu menu, View view, @@ -193,8 +246,7 @@ public void onCreateContextMenu(ContextMenu menu, View view, // Show the right type of dialog for the row in question if (row.getString("label").equals("itemType")) { // XXX don't need i18n, since this should be overcome - Toast.makeText(getApplicationContext(), "Item type cannot be changed.", - Toast.LENGTH_SHORT).show(); + Toast.makeText(getApplicationContext(), "Item type cannot be changed.", Toast.LENGTH_SHORT).show(); //removeDialog(DIALOG_ITEM_TYPE); //showDialog(DIALOG_ITEM_TYPE, row); return; @@ -204,13 +256,13 @@ public void onCreateContextMenu(ContextMenu menu, View view, } else if (row.getString("label").equals("creators")) { Log.d(TAG, "Trying to start creators activity"); Intent i = new Intent(getBaseContext(), CreatorActivity.class); - i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); startActivity(i); return; } else if (row.getString("label").equals("tags")) { Log.d(TAG, "Trying to start tag activity"); Intent i = new Intent(getBaseContext(), TagActivity.class); - i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); startActivity(i); return; } @@ -221,6 +273,7 @@ public void onCreateContextMenu(ContextMenu menu, View view, } } }); +*/ } protected Dialog onCreateDialog(int id) { @@ -334,17 +387,43 @@ public void onDestroy() { @Override public void onResume() { - if (db == null) db = new Database(this); + if (db == null) + db = new Database(this); + + refreshData(); + super.onResume(); } + + public void refreshData() { + item = Item.load(itemKey, db); + + // When an item in the view has been updated via a sync, the temporary key may have been swapped out, so we fall back on the DB ID + if (item == null) { + String itemDbId = getIntent().getStringExtra("com.mattrobertson.zotable.app.itemDbId"); + if (itemDbId == null) { + Log.d(TAG, "Failed to load item using itemKey and no dbId specified. Give up and finish activity."); + finish(); + return; + } + item = Item.loadDbId(itemDbId, db); + } + + if (item != null) { + rows = item.toBundleArray(db); + mBundleListAdapter = new BundleListAdapter(); + mBundleListAdapter.bundles = rows; + setListAdapter(mBundleListAdapter); + } + + getExpandableListView().invalidate(); + } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.zotero_menu, menu); - // Remove new item-- should be created from context of an item list - menu.removeItem(R.id.do_new); - + // Turn on delete item MenuItem del = menu.findItem(R.id.do_delete); del.setEnabled(true); @@ -356,25 +435,6 @@ public boolean onCreateOptionsMenu(Menu menu) { public boolean onOptionsItemSelected(MenuItem i) { // Handle item selection switch (i.getItemId()) { - case R.id.do_sync: - if (!ServerCredentials.check(getApplicationContext())) { - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_log_in_first), - Toast.LENGTH_SHORT).show(); - return true; - } - Log.d(TAG, "Preparing sync requests, starting with present item"); - APIRequest req; - if (APIRequest.API_CLEAN.equals(item.dirty)) { - ArrayList items = new ArrayList(); - items.add(item); - req = APIRequest.add(items); - } else { - req = APIRequest.update(item); - } - new ZoteroAPITask(getBaseContext()).execute(req); - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_started), - Toast.LENGTH_SHORT).show(); - return true; case R.id.do_prefs: startActivity(new Intent(this, SettingsActivity.class)); return true; @@ -385,6 +445,9 @@ public boolean onOptionsItemSelected(MenuItem i) { this.b = b; showDialog(DIALOG_CONFIRM_DELETE); return true; + case android.R.id.home: + onBackPressed(); + return true; default: return super.onOptionsItemSelected(i); } @@ -415,8 +478,7 @@ public int getChildrenCount(int groupPosition) { public TextView getGenericView() { // Layout parameters for the ExpandableListView - AbsListView.LayoutParams lp = new AbsListView.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, 64); + AbsListView.LayoutParams lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 64); TextView textView = new TextView(ItemDataActivity.this); textView.setLayoutParams(lp); @@ -482,20 +544,50 @@ public View getGroupView(int groupPosition, boolean isExpanded, View convertView row = convertView; } - /* Our layout has just two fields */ - TextView tvLabel = (TextView) row.findViewById(R.id.data_label); - tvLabel.setPadding(0, 0, 0, 0); + TextView tvLabel = (TextView) row.findViewById(R.id.data_label); TextView tvContent = (TextView) row.findViewById(R.id.data_content); + ImageView imgAction = (ImageView)row.findViewById(R.id.imgAction); + + if (label.equals("title")) { + imgAction.setImageResource(R.drawable.ic_edit); + imgAction.setVisibility(View.VISIBLE); + } + else if (label.equals("creators")) { + imgAction.setImageResource(R.drawable.ic_edit); + imgAction.setVisibility(View.VISIBLE); + } + else if (label.equals("tags")) { + imgAction.setImageResource(R.drawable.ic_edit); + imgAction.setVisibility(View.VISIBLE); + } + else if (label.equals("collections")) { + imgAction.setImageResource(R.drawable.ic_edit); + imgAction.setVisibility(View.VISIBLE); + } + else if (label.equals("DOI") && ! content.trim().equals("")) { + imgAction.setImageResource(R.drawable.ic_browse); + imgAction.setVisibility(View.VISIBLE); + } + else if (label.equals("url") && ! content.trim().equals("")) { + imgAction.setImageResource(R.drawable.ic_browse); + imgAction.setVisibility(View.VISIBLE); + } + else { + imgAction.setImageResource(android.R.color.transparent); + imgAction.setVisibility(View.GONE); + } + + tvLabel.setPadding(0, 0, 0, 0); tvContent.setPadding(0, 0, 0, 0); /* Since the field names are the API / internal form, we * attempt to get a localized, human-readable version. */ tvLabel.setText(Item.localizedStringForString(label)); - - if ("title".equals(label) - || "note".equals(label)) { + + if ("title".equals(label) || "note".equals(label)) { tvContent.setText(Html.fromHtml(content)); - } else { + } + else { tvContent.setText(content); } diff --git a/src/main/java/com/gimranov/zandy/app/NoteActivity.java b/src/main/java/com/mattrobertson/zotable/app/NoteActivity.java old mode 100644 new mode 100755 similarity index 86% rename from src/main/java/com/gimranov/zandy/app/NoteActivity.java rename to src/main/java/com/mattrobertson/zotable/app/NoteActivity.java index 7d67138..68fc01f --- a/src/main/java/com/gimranov/zandy/app/NoteActivity.java +++ b/src/main/java/com/mattrobertson/zotable/app/NoteActivity.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Zandy is distributed in the hope that it will be useful, + * Zotable is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app; +package com.mattrobertson.zotable.app; import android.app.Activity; import android.app.AlertDialog; @@ -32,10 +32,11 @@ import android.widget.TextView.BufferType; import android.widget.Toast; -import com.gimranov.zandy.app.data.Attachment; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.data.Item; -import com.gimranov.zandy.app.task.APIRequest; + +import com.mattrobertson.zotable.app.data.Attachment; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.task.APIRequest; /** * This Activity handles displaying and editing of notes. @@ -45,7 +46,7 @@ */ public class NoteActivity extends Activity { - private static final String TAG = "com.gimranov.zandy.app.NoteActivity"; + private static final String TAG = "NoteActivity"; static final int DIALOG_NOTE = 3; @@ -62,7 +63,7 @@ public void onCreate(Bundle savedInstanceState) { db = new Database(this); /* Get the incoming data from the calling activity */ - final String attKey = getIntent().getStringExtra("com.gimranov.zandy.app.attKey"); + final String attKey = getIntent().getStringExtra("com.mattrobertson.zotable.app.attKey"); final Attachment att = Attachment.load(attKey, db); if (att == null) { diff --git a/src/main/java/com/mattrobertson/zotable/app/PDFActivity.java b/src/main/java/com/mattrobertson/zotable/app/PDFActivity.java new file mode 100755 index 0000000..8646b4c --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/PDFActivity.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Environment; +import android.util.Log; +import android.widget.TextView; + +import com.mattrobertson.zotable.app.data.Database; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +public class PDFActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_pdf_handler); + + String filename = ""; + + try { + InputStream inputStream = getContentResolver().openInputStream(getIntent().getData()); + + File newFile = new File(Environment.getExternalStorageDirectory() + File.separator + "Download/temp.pdf"); + filename = newFile.getPath(); + + FileOutputStream output = new FileOutputStream(filename); + int bufferSize = 1024; + byte[] buffer = new byte[bufferSize]; + int len; + while ((len = inputStream.read(buffer)) != -1) { + output.write(buffer, 0, len); + } + } + catch (FileNotFoundException e) { + Log.e("ZZZ", e.getMessage()); + Intent intent = new Intent(this, Activity_Main.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + } + catch (IOException e) { + Log.e("ZZZ", e.getMessage()); + Intent intent = new Intent(this, Activity_Main.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + } + + Database db = new Database(this); + + // Handle the upload + Util.handleUpload(filename,this,db,true); + } +} \ No newline at end of file diff --git a/src/main/java/com/mattrobertson/zotable/app/Persistence.java b/src/main/java/com/mattrobertson/zotable/app/Persistence.java new file mode 100755 index 0000000..efc9cb4 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/Persistence.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.content.Context; +import android.content.SharedPreferences; + +import org.jetbrains.annotations.Nullable; + +public class Persistence { + private static final String TAG = Persistence.class.getCanonicalName(); + + private static final String FILE = "Persistence"; + + public static void write(String key, String value) { + SharedPreferences.Editor editor = Application.getInstance().getSharedPreferences(FILE, Context.MODE_PRIVATE).edit(); + editor.putString(key, value); + editor.commit(); + } + + @Nullable + public static String read(String key) { + SharedPreferences store = Application.getInstance().getSharedPreferences(FILE, Context.MODE_PRIVATE); + if (!store.contains(key)) return null; + + return store.getString(key, null); + } +} diff --git a/src/main/java/com/gimranov/zandy/app/Query.java b/src/main/java/com/mattrobertson/zotable/app/Query.java old mode 100644 new mode 100755 similarity index 63% rename from src/main/java/com/gimranov/zandy/app/Query.java rename to src/main/java/com/mattrobertson/zotable/app/Query.java index eb28796..ff5e379 --- a/src/main/java/com/gimranov/zandy/app/Query.java +++ b/src/main/java/com/mattrobertson/zotable/app/Query.java @@ -1,11 +1,27 @@ -package com.gimranov.zandy.app; +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; import java.util.ArrayList; import android.database.Cursor; import android.os.Bundle; -import com.gimranov.zandy.app.data.Database; +import com.mattrobertson.zotable.app.data.Database; /** * This class is intended to provide ways of handling queries to the database. @@ -20,7 +36,7 @@ * - normalize queries and data to let * - allow saving of queries * - * Some of this will mean changes to other parts of Zandy's data storage model; + * Some of this will mean changes to other parts of Zotable's data storage model; * specifically, the raw JSON we're using now won't get us much further. We * could in theory maintain an index with tokens drawn from the JSON that * we populate on original save... Not sure about this. diff --git a/src/main/java/com/gimranov/zandy/app/RequestActivity.java b/src/main/java/com/mattrobertson/zotable/app/RequestActivity.java old mode 100644 new mode 100755 similarity index 87% rename from src/main/java/com/gimranov/zandy/app/RequestActivity.java rename to src/main/java/com/mattrobertson/zotable/app/RequestActivity.java index 27806d0..3424547 --- a/src/main/java/com/gimranov/zandy/app/RequestActivity.java +++ b/src/main/java/com/mattrobertson/zotable/app/RequestActivity.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Zandy is distributed in the hope that it will be useful, + * Zotable is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app; +package com.mattrobertson.zotable.app; import android.app.ListActivity; import android.content.Context; @@ -29,8 +29,9 @@ import android.widget.TextView; import android.widget.Toast; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.task.APIRequest; + +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.task.APIRequest; /** * This activity exists only for debugging, at least at this point @@ -44,7 +45,7 @@ public class RequestActivity extends ListActivity { @SuppressWarnings("unused") - private static final String TAG = "com.gimranov.zandy.app.RequestActivity"; + private static final String TAG = "com.mattrobertson.zotable.app.RequestActivity"; private Database db; /** Called when the activity is first created. */ diff --git a/src/main/java/com/mattrobertson/zotable/app/SearchActivity.java b/src/main/java/com/mattrobertson/zotable/app/SearchActivity.java new file mode 100755 index 0000000..5c9c42a --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/SearchActivity.java @@ -0,0 +1,139 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.app.ListActivity; +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.util.Log; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemAdapter; + +public class SearchActivity extends ListActivity { + + private static final String TAG = "SearchActivity"; + + private Database db; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getActionBar().setDisplayHomeAsUpEnabled(true); + + db = new Database(this); + + prepareAdapter(); + + final ListView lv = getListView(); + lv.setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView parent, View view, int position, long id) { + ItemAdapter adapter = (ItemAdapter) parent.getAdapter(); + Cursor cur = adapter.getCursor(); + + if (cur.moveToPosition(position)) { + + Item item = Item.load(cur); + + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getBaseContext(), ItemDataActivity.class); + + if (item != null) { + Log.d(TAG, "Loading item data with key: " + item.getKey()); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); + i.putExtra("com.mattrobertson.zotable.app.itemDbId", item.dbId); + } + + startActivity(i); + } else { + // failed to move cursor-- show a toast + TextView tvTitle = (TextView) view.findViewById(R.id.item_title); + Toast.makeText(getApplicationContext(),getResources().getString(R.string.cant_open_item, tvTitle.getText()),Toast.LENGTH_SHORT).show(); + } + } + }); + } + + @Override + protected void onResume() { + super.onResume(); + Application.getInstance().getBus().register(this); + } + + @Override + public void onDestroy() { + ItemAdapter adapter = (ItemAdapter) getListAdapter(); + Cursor cur = adapter.getCursor(); + if (cur != null) cur.close(); + if (db != null) db.close(); + super.onDestroy(); + } + + @Override + protected void onPause() { + super.onPause(); + Application.getInstance().getBus().unregister(this); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) + onBackPressed(); + + return super.onOptionsItemSelected(item); + } + + private void prepareAdapter() { + ItemAdapter adapter = new ItemAdapter(this, prepareCursor()); + setListAdapter(adapter); + } + + private Cursor prepareCursor() { + String query = getIntent().getStringExtra("query"); + + setTitle(getResources().getString(R.string.search_results, query)); + + return getCursor(query); + } + + public Cursor getCursor(String query) { + String qLike = "%" + query + "%"; + + String[] args = {qLike,qLike,qLike}; + Cursor cursor = db.rawQuery("SELECT item_title, item_type, item_content, etag, dirty, " + + "_id, item_key, item_year, item_creator, timestamp, item_children " + + " FROM items WHERE item_title LIKE ? OR item_creator LIKE ? OR item_content LIKE ?" + + " ORDER BY item_title", + args); + + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + + return cursor; + } +} \ No newline at end of file diff --git a/src/main/java/com/gimranov/zandy/app/ServerCredentials.java b/src/main/java/com/mattrobertson/zotable/app/ServerCredentials.java old mode 100644 new mode 100755 similarity index 87% rename from src/main/java/com/gimranov/zandy/app/ServerCredentials.java rename to src/main/java/com/mattrobertson/zotable/app/ServerCredentials.java index d5c8184..6ce7521 --- a/src/main/java/com/gimranov/zandy/app/ServerCredentials.java +++ b/src/main/java/com/mattrobertson/zotable/app/ServerCredentials.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Zandy is distributed in the hope that it will be useful, + * Zotable is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app; +package com.mattrobertson.zotable.app; import java.io.File; @@ -24,16 +24,20 @@ import android.preference.PreferenceManager; import android.util.Log; -import com.gimranov.zandy.app.task.APIRequest; +import com.mattrobertson.zotable.app.task.APIRequest; public class ServerCredentials { /** Application key -- available from Zotero */ + public static final String CONSUMERKEY = "e46bd4963d8b0bc76806"; + public static final String CONSUMERSECRET = "00eae64e4f732b22fcf8"; + +/* Zandy application key public static final String CONSUMERKEY = "93a5aac13612aed2a236"; public static final String CONSUMERSECRET = "196d86bd1298cb78511c"; - +*/ /** This is the zotero:// protocol we intercept * It probably shouldn't be changed. */ - public static final String CALLBACKURL = "zotero://"; + public static final String CALLBACKURL = "zotable://"; /** This is the Zotero API server. Those who set up independent * Zotero installations will need to change this. */ @@ -77,7 +81,7 @@ public class ServerCredentials { "all_groups=write"; /* More constants */ - public static final File sBaseStorageDir = new File(Environment.getExternalStorageDirectory(), "/Android/data/com.gimranov.zandy"); + public static final File sBaseStorageDir = new File(Environment.getExternalStorageDirectory(), "/Android/data/com.mattrobertson.zotable"); public static final File sDocumentStorageDir = new File(sBaseStorageDir, "documents"); public static final File sCacheDir = new File(sBaseStorageDir, "cache"); diff --git a/src/main/java/com/gimranov/zandy/app/SettingsActivity.java b/src/main/java/com/mattrobertson/zotable/app/SettingsActivity.java old mode 100644 new mode 100755 similarity index 86% rename from src/main/java/com/gimranov/zandy/app/SettingsActivity.java rename to src/main/java/com/mattrobertson/zotable/app/SettingsActivity.java index 7608079..c408d94 --- a/src/main/java/com/gimranov/zandy/app/SettingsActivity.java +++ b/src/main/java/com/mattrobertson/zotable/app/SettingsActivity.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Zandy is distributed in the hope that it will be useful, + * Zotable is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app; +package com.mattrobertson.zotable.app; import android.app.AlertDialog; import android.app.Dialog; @@ -27,11 +27,12 @@ import android.view.View.OnClickListener; import android.widget.Button; -import com.gimranov.zandy.app.data.Database; + +import com.mattrobertson.zotable.app.data.Database; public class SettingsActivity extends PreferenceActivity implements OnClickListener { - private static final String TAG = "com.gimranov.zandy.app.SettingsActivity"; + private static final String TAG = "SettingsActivity"; static final int DIALOG_CONFIRM_DELETE = 5; @@ -40,7 +41,7 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.settings); - setContentView(R.layout.preferences); + setContentView(R.layout.preferences_old); Button requestButton = (Button) findViewById(R.id.requestQueue); requestButton.setOnClickListener(this); @@ -48,7 +49,7 @@ public void onCreate(Bundle savedInstanceState) { Button resetButton = (Button) findViewById(R.id.resetDatabase); resetButton.setOnClickListener(this); } - + public void onClick(View v) { if (v.getId() == R.id.requestQueue) { Intent i = new Intent(getApplicationContext(), RequestActivity.class); diff --git a/src/main/java/com/mattrobertson/zotable/app/SyncEvent.java b/src/main/java/com/mattrobertson/zotable/app/SyncEvent.java new file mode 100755 index 0000000..cc56f5c --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/SyncEvent.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +public class SyncEvent { + private static final String TAG = SyncEvent.class.getCanonicalName(); + + public static final int COMPLETE_CODE = 1; + + public static final SyncEvent COMPLETE = new SyncEvent(COMPLETE_CODE); + + private int status; + + public SyncEvent(int status) { + this.status = status; + } + + public int getStatus() { + return status; + } +} diff --git a/src/main/java/com/mattrobertson/zotable/app/TagActivity.java b/src/main/java/com/mattrobertson/zotable/app/TagActivity.java new file mode 100755 index 0000000..4a480ca --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/TagActivity.java @@ -0,0 +1,262 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import java.util.ArrayList; +import java.util.List; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ListActivity; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.text.Editable; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.ArrayAdapter; +import android.widget.BaseAdapter; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.TextView.BufferType; + + +import com.getbase.floatingactionbutton.FloatingActionButton; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemCollection; + +/** + * This Activity handles displaying and editing tags. It works almost the same as + * ItemDataActivity, using a simple ArrayAdapter on Bundles with the tag info. + * + * @author ajlyon + * + */ +public class TagActivity extends Activity { + + private static final String TAG = "TagActivity"; + + static final int DIALOG_TAG = 3; + static final int DIALOG_CONFIRM_NAVIGATE = 4; + + private Item item; + + ListView lvTags; + TagAdapter adapter; + ArrayList rows; + + String itemKey = ""; + + private Database db; + + protected Bundle b = new Bundle(); + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.tag_activity); + + db = new Database(this); + + /* Get the incoming data from the calling activity */ + itemKey = getIntent().getStringExtra("com.mattrobertson.zotable.app.itemKey"); + Item item = Item.load(itemKey, db); + this.item = item; + + this.setTitle(getResources().getString(R.string.tags_for_item, item.getTitle())); + + lvTags = (ListView)findViewById(R.id.lvTags); + + rows = item.tagsToBundleArray(); + + adapter = new TagAdapter(rows); + lvTags.setAdapter(adapter); + + lvTags.setTextFilterEnabled(true); + lvTags.setOnItemClickListener(new OnItemClickListener() { + @SuppressWarnings("unchecked") + public void onItemClick(AdapterView parent, View view, int position, long id) { + + } + }); + + FloatingActionButton fab = (FloatingActionButton)findViewById(R.id.btnFab); + + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + + AlertDialog.Builder builder = new AlertDialog.Builder(TagActivity.this); + builder.setTitle("New tag"); + + final EditText etTagName = new EditText(TagActivity.this); + etTagName.setTextColor(getResources().getColor(R.color.white)); + + builder.setView(etTagName); + builder.setPositiveButton(getResources().getString(R.string.ok), new DialogInterface.OnClickListener() { + @SuppressWarnings("unchecked") + public void onClick(DialogInterface dialog, int whichButton) { + Item item = Item.load(itemKey, db); + item.addTag(etTagName.getText().toString()); + item.save(db); + refreshData(); + } + }); + + builder.setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { + @SuppressWarnings("unchecked") + public void onClick(DialogInterface dialog, int whichButton) { + + } + }); + + + final Dialog dialog = builder.create(); + + dialog.show(); + } + }); + } + + @Override + public void onDestroy() { + if (db != null) + db.close(); + + super.onDestroy(); + } + + @Override + public void onResume() { + if (db == null) + db = new Database(this); + + super.onResume(); + } + + public void refreshData() { + item = Item.load(itemKey, db); + rows = item.tagsToBundleArray(); + + adapter.setData(rows); + adapter.notifyDataSetChanged(); + + lvTags.invalidate(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.zotero_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + switch (item.getItemId()) { + case R.id.do_prefs: + startActivity(new Intent(this, SettingsActivity.class)); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + class TagAdapter extends BaseAdapter { + private ArrayList mList; + + public TagAdapter(ArrayList list) { + mList = list; + } + + public void setData(ArrayList newList) { + mList = newList; + } + + @Override + public int getCount() { + return mList.size(); + } + + @Override + public Object getItem(int pos) { + return mList.get(pos); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View row; + + // We are reusing views, but we need to initialize it if null + if (null == convertView) { + LayoutInflater inflater = getLayoutInflater(); + row = inflater.inflate(R.layout.list_data, null); + } else { + row = convertView; + } + + /* Our layout has just two fields */ + TextView tvLabel = (TextView) row.findViewById(R.id.data_label); + final TextView tvContent = (TextView) row.findViewById(R.id.data_content); + + if (((Bundle)getItem(position)).getInt("type") == 1) + tvLabel.setText(getResources().getString(R.string.tag_auto)); + else + tvLabel.setText(getResources().getString(R.string.tag_user)); + tvContent.setText(((Bundle) getItem(position)).getString("tag")); + + ImageView imgRemove = (ImageView) row.findViewById(R.id.imgAction); + imgRemove.setImageResource(R.drawable.ic_delete); + imgRemove.setVisibility(View.VISIBLE); + + imgRemove.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + TagActivity.this.runOnUiThread(new Runnable() { + public void run() { + item.removeTag(tvContent.getText().toString()); + item.save(db); + refreshData(); + } + }); + } + }); + + return row; + } + } +} diff --git a/src/main/java/com/mattrobertson/zotable/app/TagItemsActivity.java b/src/main/java/com/mattrobertson/zotable/app/TagItemsActivity.java new file mode 100755 index 0000000..31c3e21 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/TagItemsActivity.java @@ -0,0 +1,610 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ListActivity; +import android.app.ProgressDialog; +import android.app.SearchManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.support.v4.widget.SwipeRefreshLayout; +import android.text.Editable; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.SearchView; +import android.widget.TextView; +import android.widget.Toast; + +import com.getbase.floatingactionbutton.FloatingActionButton; +import com.google.zxing.integration.android.IntentIntegrator; +import com.google.zxing.integration.android.IntentResult; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemAdapter; +import com.mattrobertson.zotable.app.data.ItemCollection; +import com.mattrobertson.zotable.app.task.APIRequest; +import com.mattrobertson.zotable.app.task.ZoteroAPITask; +import com.squareup.otto.Subscribe; + +import org.apache.http.util.ByteArrayBuffer; +import org.jetbrains.annotations.Nullable; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; + +public class TagItemsActivity extends ListActivity { + + private static final String TAG = "TagItemsActivity"; + + static final int DIALOG_VIEW = 0; + static final int DIALOG_NEW = 1; + static final int DIALOG_SORT = 2; + static final int DIALOG_IDENTIFIER = 3; + static final int DIALOG_PROGRESS = 6; + + private String tagName; + + private String query; + private Database db; + + private ProgressDialog mProgressDialog; + private ProgressThread progressThread; + + protected Bundle b = new Bundle(); + + /** + * Refreshes the current list adapter + */ + private void refreshView() { + ItemAdapter adapter = (ItemAdapter) getListAdapter(); + if (adapter == null) return; + + Cursor newCursor = prepareCursor(); + adapter.changeCursor(newCursor); + adapter.notifyDataSetChanged(); + Log.d(TAG, "refreshing view on request"); + } + + /** + * Called when the activity is first created. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + db = new Database(this); + + setContentView(R.layout.tag_items); + + Intent intent = getIntent(); + tagName = intent.getStringExtra("com.mattrobertson.zotable.app.tagName"); + + prepareAdapter(); + + ItemAdapter adapter = (ItemAdapter) getListAdapter(); + Cursor cur = adapter.getCursor(); + + final ListView lv = getListView(); + lv.setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView parent, View view, int position, long id) { + // If we have a click on an item, do something... + ItemAdapter adapter = (ItemAdapter) parent.getAdapter(); + Cursor cur = adapter.getCursor(); + // Place the cursor at the selected item + if (cur.moveToPosition(position)) { + // and load an activity for the item + Item item = Item.load(cur); + + Log.d(TAG, "Loading item data with key: " + item.getKey()); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); + i.putExtra("com.mattrobertson.zotable.app.itemDbId", item.dbId); + startActivity(i); + } else { + // failed to move cursor-- show a toast + TextView tvTitle = (TextView) view.findViewById(R.id.item_title); + Toast.makeText(getApplicationContext(), + getResources().getString(R.string.cant_open_item, tvTitle.getText()), + Toast.LENGTH_SHORT).show(); + } + } + }); + + // Create floating action buttons + FloatingActionButton fabScan = (FloatingActionButton)findViewById(R.id.btnFabScan); + FloatingActionButton fabIsbn = (FloatingActionButton)findViewById(R.id.btnFabIsbn); + FloatingActionButton fabManual = (FloatingActionButton)findViewById(R.id.btnFabManual); + + fabScan.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + scanBarcode(TagItemsActivity.this); + } + }); + + fabIsbn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + removeDialog(DIALOG_IDENTIFIER); + showDialog(DIALOG_IDENTIFIER); + } + }); + + fabManual.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + removeDialog(DIALOG_NEW); + showDialog(DIALOG_NEW); + } + }); + } + + @Override + protected void onResume() { + super.onResume(); + Application.getInstance().getBus().register(this); + refreshView(); + } + + @Override + public void onDestroy() { + ItemAdapter adapter = (ItemAdapter) getListAdapter(); + Cursor cur = adapter.getCursor(); + if (cur != null) cur.close(); + if (db != null) db.close(); + super.onDestroy(); + } + + @Override + protected void onPause() { + super.onPause(); + Application.getInstance().getBus().unregister(this); + } + + private void prepareAdapter() { + ItemAdapter adapter = new ItemAdapter(this, prepareCursor()); + setListAdapter(adapter); + } + + private Cursor prepareCursor() { + Cursor cursor; + // Be ready for a search + Intent intent = getIntent(); + + tagName = ""; + tagName = intent.getStringExtra("com.mattrobertson.zotable.app.tagName"); + + this.setTitle("Tag: " + tagName); + + String[] args = {tagName}; + + cursor = db.rawQuery("SELECT item_title, item_type, item_content, etag, dirty, I._id, " + + "item_key, item_year, item_creator, timestamp, item_children FROM items AS I " + + "JOIN tags AS T ON I._id=T.item_id " + + "WHERE T.tag=? " + + "ORDER BY item_title",args); +/* + String[] args = { tagName }; + Log.i(TAG, "Loading items with tag: "+tagName); + cursor = db.query("tags", Database.TAGCOLS, "tag=?", args, null, null, null, null); +*/ + //} + return cursor; + } + + protected Dialog onCreateDialog(int id) { + switch (id) { + case DIALOG_NEW: + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getResources().getString(R.string.item_type)) + // XXX i18n + .setItems(Item.ITEM_TYPES_EN, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int pos) { + Item item = new Item(getBaseContext(), Item.ITEM_TYPES[pos]); + item.dirty = APIRequest.API_DIRTY; + item.save(db); + + Log.d(TAG, "Loading item data with key: " + item.getKey()); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); + startActivity(i); + } + }); + AlertDialog dialog = builder.create(); + return dialog; + + case DIALOG_PROGRESS: + mProgressDialog = new ProgressDialog(this); + mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + mProgressDialog.setIndeterminate(true); + mProgressDialog.setMessage(getResources().getString(R.string.identifier_looking_up)); + return mProgressDialog; + case DIALOG_IDENTIFIER: + final EditText input = new EditText(this); + input.setHint(getResources().getString(R.string.identifier_hint)); + + final TagItemsActivity current = this; + + dialog = new AlertDialog.Builder(this) + .setTitle(getResources().getString(R.string.identifier_message)) + .setView(input) + .setPositiveButton(getResources().getString(R.string.menu_search), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + Editable value = input.getText(); + // run search + Bundle c = new Bundle(); + c.putString("mode", "isbn"); + c.putString("identifier", value.toString()); + removeDialog(DIALOG_PROGRESS); + TagItemsActivity.this.b = c; + showDialog(DIALOG_PROGRESS); + } + }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // do nothing + } + }).create(); + return dialog; + default: + return null; + } + } + + @Override + protected void onPrepareDialog(int id, Dialog dialog) { + switch (id) { + case DIALOG_PROGRESS: + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.zotero_menu, menu); + + // Turn on search item + MenuItem search = menu.findItem(R.id.do_search); + search.setEnabled(true); + search.setVisible(true); + + SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); + SearchView searchView = (SearchView) menu.findItem(R.id.do_search).getActionView(); + searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + switch (item.getItemId()) { + case R.id.do_search: + onSearchRequested(); + return true; + case R.id.do_prefs: + Intent i = new Intent(getBaseContext(), SettingsActivity.class); + startActivity(i); + return true; + case R.id.do_sort: + removeDialog(DIALOG_SORT); + showDialog(DIALOG_SORT); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + /* Handling the ListView and keeping it up to date */ + public Cursor getCursor() { + Cursor cursor = db.query("tags", Database.TAGCOLS, null, null, null, null, null, null); + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + return cursor; + } + + public Cursor getCursor(String query) { + String[] args = {"%" + query + "%", "%" + query + "%"}; + Cursor cursor = db.rawQuery("SELECT item_title, item_type, item_content, etag, dirty, " + + "_id, item_key, item_year, item_creator, timestamp, item_children " + + " FROM items WHERE item_title LIKE ? OR item_creator LIKE ?" + + " ORDER BY item_title", + args); + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + return cursor; + } + + public Cursor getCursor(Query query) { + return query.query(db); + } + + /* Thread and helper to run lookups */ + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + Log.d(TAG, "_____________________on_activity_result"); + + IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); + if (scanResult != null) { + // handle scan result + Bundle b = new Bundle(); + b.putString("mode", "isbn"); + b.putString("identifier", scanResult.getContents()); + if (scanResult != null + && scanResult.getContents() != null) { + Log.d(TAG, b.getString("identifier")); + progressThread = new ProgressThread(handler, b); + progressThread.start(); + this.b = b; + removeDialog(DIALOG_PROGRESS); + showDialog(DIALOG_PROGRESS); + } else { + Toast.makeText(getApplicationContext(), + getResources().getString(R.string.identifier_scan_failed), + Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(getApplicationContext(), + getResources().getString(R.string.identifier_scan_failed), + Toast.LENGTH_SHORT).show(); + } + } + + final Handler handler = new Handler() { + public void handleMessage(Message msg) { + Log.d(TAG, "______________________handle_message"); + if (ProgressThread.STATE_DONE == msg.arg2) { + Bundle data = msg.getData(); + String itemKey = data.getString("itemKey"); + if (itemKey != null) { + + mProgressDialog.dismiss(); + mProgressDialog = null; + + Log.d(TAG, "Loading new item data with key: " + itemKey); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", itemKey); + startActivity(i); + } + return; + } + + if (ProgressThread.STATE_PARSING == msg.arg2) { + mProgressDialog.setMessage(getResources().getString(R.string.identifier_processing)); + return; + } + + if (ProgressThread.STATE_ERROR == msg.arg2) { + dismissDialog(DIALOG_PROGRESS); + Toast.makeText(getApplicationContext(), + getResources().getString(R.string.identifier_lookup_failed), + Toast.LENGTH_SHORT).show(); + progressThread.setState(ProgressThread.STATE_DONE); + return; + } + } + }; + + private class ProgressThread extends Thread { + Handler mHandler; + Bundle arguments; + final static int STATE_DONE = 5; + final static int STATE_FETCHING = 1; + final static int STATE_PARSING = 6; + final static int STATE_ERROR = 7; + + int mState; + + ProgressThread(Handler h, Bundle b) { + mHandler = h; + arguments = b; + Log.d(TAG, "_____________________thread_constructor"); + } + + public void run() { + Log.d(TAG, "_____________________thread_run"); + mState = STATE_FETCHING; + + // Setup + String identifier = arguments.getString("identifier"); + String mode = arguments.getString("mode"); + URL url; + String urlstring; + + String response = ""; + + if ("isbn".equals(mode)) { + urlstring = "http://xisbn.worldcat.org/webservices/xid/isbn/" + + identifier + + "?method=getMetadata&fl=*&format=json&count=1"; + } else { + urlstring = ""; + } + + try { + Log.d(TAG, "Fetching from: " + urlstring); + url = new URL(urlstring); + + /* Open a connection to that URL. */ + URLConnection ucon = url.openConnection(); + /* + * Define InputStreams to read from the URLConnection. + */ + InputStream is = ucon.getInputStream(); + BufferedInputStream bis = new BufferedInputStream(is, 16000); + + ByteArrayBuffer baf = new ByteArrayBuffer(50); + int current = 0; + + /* + * Read bytes to the Buffer until there is nothing more to read(-1). + */ + while (mState == STATE_FETCHING + && (current = bis.read()) != -1) { + baf.append((byte) current); + } + response = new String(baf.toByteArray()); + Log.d(TAG, response); + + + } catch (IOException e) { + Log.e(TAG, "Error: ", e); + } + + Message msg = mHandler.obtainMessage(); + msg.arg2 = STATE_PARSING; + mHandler.sendMessage(msg); + + /* + * { + "stat":"ok", + "list":[{ + "url":["http://www.worldcat.org/oclc/177669176?referer=xid"], + "publisher":"O'Reilly", + "form":["BA"], + "lccn":["2004273129"], + "lang":"eng", + "city":"Sebastopol, CA", + "author":"by Mark Lutz and David Ascher.", + "ed":"2nd ed.", + "year":"2003", + "isbn":["0596002815"], + "title":"Learning Python", + "oclcnum":["177669176", +.. + "748093898"]}]} + */ + + // This is OCLC-specific logic + try { + JSONObject result = new JSONObject(response); + + if (!result.getString("stat").equals("ok")) { + Log.e(TAG, "Error response received"); + msg = mHandler.obtainMessage(); + msg.arg2 = STATE_ERROR; + mHandler.sendMessage(msg); + return; + } + + result = result.getJSONArray("list").getJSONObject(0); + String form = result.getJSONArray("form").getString(0); + String type; + + if ("AA".equals(form)) type = "audioRecording"; + else if ("VA".equals(form)) type = "videoRecording"; + else if ("FA".equals(form)) type = "film"; + else type = "book"; + + // TODO Fix this + type = "book"; + + Item item = new Item(getBaseContext(), type); + + JSONObject content = item.getContent(); + + if (result.has("lccn")) { + String lccn = "LCCN: " + result.getJSONArray("lccn").getString(0); + content.put("extra", lccn); + } + + if (result.has("isbn")) { + content.put("ISBN", result.getJSONArray("isbn").getString(0)); + } + + content.put("title", result.optString("title", "")); + content.put("place", result.optString("city", "")); + content.put("edition", result.optString("ed", "")); + content.put("language", result.optString("lang", "")); + content.put("publisher", result.optString("publisher", "")); + content.put("date", result.optString("year", "")); + + item.setTitle(result.optString("title", "")); + item.setYear(result.optString("year", "")); + + String author = result.optString("author", ""); + + item.setCreatorSummary(author); + JSONArray array = new JSONArray(); + JSONObject member = new JSONObject(); + member.accumulate("creatorType", "author"); + member.accumulate("name", author); + array.put(member); + content.put("creators", array); + + item.setContent(content); + item.save(db); + + msg = mHandler.obtainMessage(); + Bundle data = new Bundle(); + data.putString("itemKey", item.getKey()); + msg.setData(data); + msg.arg2 = STATE_DONE; + mHandler.sendMessage(msg); + return; + } catch (JSONException e) { + Log.e(TAG, "exception parsing response", e); + msg = mHandler.obtainMessage(); + msg.arg2 = STATE_ERROR; + mHandler.sendMessage(msg); + return; + } + } + + public void setState(int state) { + mState = state; + } + } + + public void scanBarcode(TagItemsActivity current) { + // If we're about to download from Google play, cancel that dialog + // and prompt from Amazon if we're on an Amazon device + IntentIntegrator integrator = new IntentIntegrator(current); + @Nullable AlertDialog producedDialog = integrator.initiateScan(); + if (producedDialog != null && "amazon".equals(BuildConfig.FLAVOR)) { + producedDialog.dismiss(); + AmazonZxingGlue.showDownloadDialog(current); + } + } +} diff --git a/src/main/java/com/mattrobertson/zotable/app/Util.java b/src/main/java/com/mattrobertson/zotable/app/Util.java new file mode 100755 index 0000000..6e43886 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/Util.java @@ -0,0 +1,105 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.util.Log; + +import com.mattrobertson.zotable.app.data.Creator; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.task.APIRequest; + +import org.pdfparse.model.PDFDocCatalog; +import org.pdfparse.model.PDFDocInfo; +import org.pdfparse.model.PDFDocument; + +public class Util { + private static final String TAG = Util.class.getCanonicalName(); + + public static final String DOI_PREFIX = "http://dx.doi.org/"; + + public static String doiToUri(String doi) { + if (isDoi(doi)) { + return DOI_PREFIX + doi.replaceAll("^doi:", ""); + } + return doi; + } + + public static boolean isDoi(String doi) { + return (doi.startsWith("doi:") || doi.startsWith("10.")); + } + + public static void handleUpload(String filename, final Context context, final Database db, final boolean clearStack) { + + final String title; + final String author; + final String keywords; + + try { + // Create document object. Open file + PDFDocument doc = new PDFDocument(filename); + + // Get document structure elements + PDFDocInfo info = doc.getDocumentInfo(); + PDFDocCatalog cat = doc.getDocumentCatalog(); + + title = info.getTitle(); + author = info.getAuthor(); + keywords = info.getSubject(); + + } catch (Exception e) { + Log.e("ZZZ", e.getMessage()); + return; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(context.getResources().getString(R.string.item_type)) + // XXX i18n + .setItems(Item.ITEM_TYPES_EN, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int pos) { + Item item = new Item(context, Item.ITEM_TYPES[pos]); + item.dirty = APIRequest.API_DIRTY; + item.setTitle(title); + item.save(db); + + Item.setCreator(item.getKey(), new Creator("author", author, true), 0, db); + item = Item.load(item.getKey(),db); + + String[] arrKeywords = keywords.split(","); + for (String keyword : arrKeywords) + item.addTag(keyword.trim()); + + item.save(db); + + Log.d(TAG, "Loading item data with key: " + item.getKey()); + + // We create and issue a specified intent with the necessary data + Intent i = new Intent(context, ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); + if (clearStack) + i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + context.startActivity(i); + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + } +} diff --git a/src/main/java/com/gimranov/zandy/app/XMLResponseParser.java b/src/main/java/com/mattrobertson/zotable/app/XMLResponseParser.java old mode 100644 new mode 100755 similarity index 94% rename from src/main/java/com/gimranov/zandy/app/XMLResponseParser.java rename to src/main/java/com/mattrobertson/zotable/app/XMLResponseParser.java index 49083af..7060a97 --- a/src/main/java/com/gimranov/zandy/app/XMLResponseParser.java +++ b/src/main/java/com/mattrobertson/zotable/app/XMLResponseParser.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Zandy is distributed in the hope that it will be useful, + * Zotable is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app; +package com.mattrobertson.zotable.app; import android.sax.Element; import android.sax.ElementListener; @@ -24,12 +24,11 @@ import android.util.Log; import android.util.Xml; -import com.crashlytics.android.Crashlytics; -import com.gimranov.zandy.app.data.Attachment; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.data.Item; -import com.gimranov.zandy.app.data.ItemCollection; -import com.gimranov.zandy.app.task.APIRequest; +import com.mattrobertson.zotable.app.data.Attachment; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemCollection; +import com.mattrobertson.zotable.app.task.APIRequest; import org.json.JSONException; import org.json.JSONObject; @@ -40,7 +39,7 @@ import java.util.ArrayList; public class XMLResponseParser extends DefaultHandler { - private static final String TAG = "com.gimranov.zandy.app.XMLResponseParser"; + private static final String TAG = "XMLResponseParser"; private InputStream input; private Item item; @@ -72,14 +71,14 @@ public XMLResponseParser(InputStream in, APIRequest request) { followNext = true; input = in; this.request = request; - // Initialize the request queue if needed + // Initialize the request buildDirtyQueue if needed if (queue == null) queue = new ArrayList(); } public XMLResponseParser(APIRequest request) { followNext = true; this.request = request; - // Initialize the request queue if needed + // Initialize the request buildDirtyQueue if needed if (queue == null) queue = new ArrayList(); } @@ -134,7 +133,7 @@ public void start(Attributes attributes) { Log.d(TAG, "Key extraction failed from root; maybe this isn't a collection listing?"); } } - // If there are more items, queue them up to be handled too + // If there are more items, buildDirtyQueue them up to be handled too if (rel.contains("next")) { Log.d(TAG, "Found continuation: "+href); APIRequest req = new APIRequest(href, "get", null); @@ -155,7 +154,9 @@ public void start(Attributes attributes) { } public void end() { + Log.d("ZZZ-zotable", "XMLResponse end()"); if (items == true) { + Log.d("ZZZ-zotable", "XMLResponse end() items == true"); if (updateKey != null && updateType != null && updateType.equals("item")) { // We have an incoming new version of an item Item existing = Item.load(updateKey, db); @@ -190,6 +191,7 @@ public void end() { } } else { item.dirty = APIRequest.API_CLEAN; + Log.d("ZZZ-zotable", "API_CLEAN 3"); attachment.dirty = APIRequest.API_CLEAN; if ((attachment.url != null && !"".equals(attachment.url)) || attachment.content.optInt("linkMode") == Attachment.MODE_IMPORTED_FILE @@ -235,6 +237,7 @@ public void end() { } if (!items) { + Log.d("ZZZ-zotable-false", "items==false"); if (updateKey != null && updateType != null && updateType.equals("collection")) { // We have an incoming new version of a collection ItemCollection existing = ItemCollection.load(updateKey, db); @@ -388,7 +391,6 @@ public void end(String body) { db.close(); } catch (Exception e) { Log.e(TAG, "exception loading content", e); - Crashlytics.logException(new Exception("Exception parsing data", e)); } } } diff --git a/src/main/java/com/gimranov/zandy/app/data/Attachment.java b/src/main/java/com/mattrobertson/zotable/app/data/Attachment.java old mode 100644 new mode 100755 similarity index 85% rename from src/main/java/com/gimranov/zandy/app/data/Attachment.java rename to src/main/java/com/mattrobertson/zotable/app/data/Attachment.java index 0c73f7b..01d6610 --- a/src/main/java/com/gimranov/zandy/app/data/Attachment.java +++ b/src/main/java/com/mattrobertson/zotable/app/data/Attachment.java @@ -1,4 +1,20 @@ -package com.gimranov.zandy.app.data; +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app.data; import java.util.ArrayList; import java.util.UUID; @@ -10,7 +26,7 @@ import android.database.Cursor; import android.util.Log; -import com.gimranov.zandy.app.task.APIRequest; +import com.mattrobertson.zotable.app.task.APIRequest; public class Attachment { @@ -41,7 +57,7 @@ public class Attachment { */ public JSONObject content; - private static final String TAG = "com.gimranov.zandy.app.data.Attachment"; + private static final String TAG = "Attachment"; public static final int AVAILABLE = 1; public static final int LOCAL = 2; @@ -157,10 +173,10 @@ public void delete(Database db) { */ public static void queue(Database db) { if (queue == null) { - // Initialize the queue if necessary + // Initialize the buildDirtyQueue if necessary queue = new ArrayList(); } - Log.d(TAG, "Clearing attachment dirty queue before repopulation"); + Log.d(TAG, "Clearing attachment dirty buildDirtyQueue before repopulation"); queue.clear(); Attachment attachment; String[] cols = Database.ATTCOLS; @@ -175,7 +191,7 @@ public static void queue(Database db) { } do { - Log.d(TAG, "Adding attachment to dirty queue"); + Log.d(TAG, "Adding attachment to dirty buildDirtyQueue"); attachment = load(cur); queue.add(attachment); } while (cur.moveToNext() != false); diff --git a/src/main/java/com/mattrobertson/zotable/app/data/CollectionAdapter.java b/src/main/java/com/mattrobertson/zotable/app/data/CollectionAdapter.java new file mode 100755 index 0000000..1167d6a --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/data/CollectionAdapter.java @@ -0,0 +1,227 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app.data; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.media.Image; +import android.text.Layout; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.ImageView; +import android.widget.ResourceCursorAdapter; +import android.widget.TextView; +import android.widget.ToggleButton; + + +import com.mattrobertson.zotable.app.CollectionActivity; +import com.mattrobertson.zotable.app.R; +import com.mattrobertson.zotable.app.task.APIRequest; + +/** + * Exposes collection to be displayed by a ListView + * @author ajlyon + * + */ +public class CollectionAdapter extends ResourceCursorAdapter { + public static final String TAG = "CollectionAdapter"; + + public Context context; + LayoutInflater mInflater; + + Cursor cursor; + + public CollectionAdapter(Context context, Cursor cursor) { + super(context, R.layout.list_collection, cursor, false); + this.context = context; + this.cursor = cursor; + mInflater = ((Activity)context).getLayoutInflater(); + } + + /** + * Call this when the data has been updated-- it refreshes the cursor and notifies of the change + */ + public void notifyDataSetChanged() { + super.notifyDataSetChanged(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) + { + cursor = (Cursor)getItem(position); + cursor.moveToPosition(position); + + final ViewHolder holder; + + if ( convertView == null ) + { + // no view at this position - create a new one + convertView = mInflater.inflate(R.layout.list_collection, null); + holder = new ViewHolder(); + + holder.tvTitle = (TextView)convertView.findViewById(R.id.collection_title); + holder.tvInfo = (TextView)convertView.findViewById(R.id.collection_info); + holder.imgFav = (ImageView)convertView.findViewById(R.id.imgFavorite); + holder.imgExpand = (ImageView)convertView.findViewById(R.id.imgExpand); + + convertView.setTag (holder); + } + else + { + // recycle a View that already exists + holder = (ViewHolder) convertView.getTag (); + } + + final Database db = new Database(context); + + final ItemCollection collection = ItemCollection.load(cursor); + + if (collection.isFavorite()) + holder.imgFav.setImageResource(R.drawable.ic_star_filled); + else + holder.imgFav.setImageResource(R.drawable.ic_star_empty); + + // # of subcollections - used to display # & to determine expansion + int subSize = collection.getSubcollections(db).size(); + + holder.tvTitle.setText(collection.getTitle()); + + StringBuilder sb = new StringBuilder(); + sb.append(collection.getSize() + " items"); + sb.append("; " + subSize + " subcollections"); + + if(!collection.dirty.equals(APIRequest.API_CLEAN)) + sb.append("; "+collection.dirty); + + holder.tvInfo.setText(sb.toString()); + + // Allow expansion to subcollections? + if (subSize > 0) { + holder.imgExpand.setVisibility(View.VISIBLE); + + holder.imgExpand.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent i = new Intent(context, CollectionActivity.class); + i.putExtra("com.mattrobertson.zotable.app.collectionKey", collection.getKey()); + context.startActivity(i); + } + }); + } + else { + holder.imgExpand.setVisibility(View.GONE); + } + + // Listener for "Fav" star + holder.imgFav.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (collection.isFavorite()) { + holder.imgFav.setImageResource(R.drawable.ic_star_empty); + collection.setFavorite(false); + collection.save(db); + } else { + holder.imgFav.setImageResource(R.drawable.ic_star_filled); + collection.setFavorite(true); + collection.save(db); + } + } + }); + + + db.close(); + + return convertView; + } + + static class ViewHolder{ + TextView tvTitle; + TextView tvInfo; + ImageView imgFav; + ImageView imgExpand; + } + + public View newView(Context context, Cursor cur, ViewGroup parent) { + LayoutInflater li = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + return li.inflate(R.layout.list_collection, parent, false); + } + + @Override + public void bindView(View view, final Context context, Cursor cursor) { + TextView tvTitle = (TextView)view.findViewById(R.id.collection_title); + TextView tvInfo = (TextView)view.findViewById(R.id.collection_info); + final ImageView imgFav = (ImageView)view.findViewById(R.id.imgFavorite); + final ImageView imgExpand = (ImageView)view.findViewById(R.id.imgExpand); + + final Database db = new Database(context); + + final ItemCollection collection = ItemCollection.load(cursor); + + if (collection.isFavorite()) + imgFav.setImageResource(R.drawable.ic_star_filled); + else + imgFav.setImageResource(R.drawable.ic_star_empty); + + // # of subcollections - used to display # & to determine expansion + int subSize = collection.getSubcollections(db).size(); + + tvTitle.setText(collection.getTitle()); + StringBuilder sb = new StringBuilder(); + sb.append(collection.getSize() + " items"); + sb.append("; " + subSize + " subcollections"); + if(!collection.dirty.equals(APIRequest.API_CLEAN)) + sb.append("; "+collection.dirty); + tvInfo.setText(sb.toString()); + + // Allow expansion to subcollections? + if (subSize > 0) { + imgExpand.setVisibility(View.VISIBLE); + + imgExpand.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent i = new Intent(context, CollectionActivity.class); + i.putExtra("com.mattrobertson.zotable.app.collectionKey", collection.getKey()); + context.startActivity(i); + } + }); + } + + // Listener for "Fav" star + imgFav.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (collection.isFavorite()) { + imgFav.setImageResource(R.drawable.ic_star_empty); + collection.setFavorite(false); + collection.save(db); + } else { + imgFav.setImageResource(R.drawable.ic_star_filled); + collection.setFavorite(true); + collection.save(db); + } + } + }); + + + db.close(); + } +} diff --git a/src/main/java/com/gimranov/zandy/app/data/Creator.java b/src/main/java/com/mattrobertson/zotable/app/data/Creator.java old mode 100644 new mode 100755 similarity index 89% rename from src/main/java/com/gimranov/zandy/app/data/Creator.java rename to src/main/java/com/mattrobertson/zotable/app/data/Creator.java index 6f8e975..dd09ba2 --- a/src/main/java/com/gimranov/zandy/app/data/Creator.java +++ b/src/main/java/com/mattrobertson/zotable/app/data/Creator.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Zandy is distributed in the hope that it will be useful, + * Zotable is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app.data; +package com.mattrobertson.zotable.app.data; import org.json.JSONException; import org.json.JSONObject; @@ -33,7 +33,7 @@ public class Creator { private int dbId; - public static final String TAG = "com.gimranov.zandy.app.data.Creator"; + public static final String TAG = "Creator"; /** * A Creator, given type, a single string, and a boolean mode. diff --git a/src/main/java/com/gimranov/zandy/app/data/Database.java b/src/main/java/com/mattrobertson/zotable/app/data/Database.java old mode 100644 new mode 100755 similarity index 83% rename from src/main/java/com/gimranov/zandy/app/data/Database.java rename to src/main/java/com/mattrobertson/zotable/app/data/Database.java index ddeeaab..8aa38ee --- a/src/main/java/com/gimranov/zandy/app/data/Database.java +++ b/src/main/java/com/mattrobertson/zotable/app/data/Database.java @@ -1,4 +1,20 @@ -package com.gimranov.zandy.app.data; +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app.data; import android.content.Context; import android.database.Cursor; @@ -9,20 +25,21 @@ import android.util.Log; public class Database { - private static final String TAG = "com.gimranov.zandy.app.data.Database"; + private static final String TAG = "Database"; public static final String[] ITEMCOLS = {"item_title", "item_type", "item_content", "etag", "dirty", "_id", "item_key", "item_year", "item_creator", "timestamp", "item_children"}; public static final String[] COLLCOLS = {"collection_name", "collection_parent", "etag", "dirty", "_id", "collection_key", - "collection_size", "timestamp"}; + "collection_size", "timestamp","fav"}; public static final String[] ATTCOLS = { "_id", "attachment_key", "item_key", "title", "filename", "url", "status", "etag", "dirty", "content" }; public static final String[] REQUESTCOLS = {"_id", "uuid", "type", "query", "key", "method", "disposition", "if_match", "update_key", "update_type", "created", "last_attempt", "status", "body"}; + public static final String[] TAGCOLS = {"_id","item_id","tag"}; // the database version; increment to call update private static final int DATABASE_VERSION = 20; @@ -41,7 +58,7 @@ public void resetAllData() { Log.d(TAG, "Dropping tables to reset database"); String[] tables = {"collections", "items", "creators", "children", "itemtocreators", "itemtocollections", "deleteditems", "attachments", - "apirequests", "notes"}; + "apirequests", "notes","tags"}; String[] args = {}; for (int i = 0; i < tables.length; i++) { rawQuery("DROP TABLE IF EXISTS " + tables[i], args); @@ -114,7 +131,7 @@ private static class DatabaseOpenHelper extends SQLiteOpenHelper { // table creation statements // for temp table creation to work, must have (_id as first field private static final String COLLECTIONS_CREATE = - "create table collections"+ + "create table collections"+ " (_id integer primary key autoincrement, " + "collection_name text not null, " + "collection_key string unique, " + @@ -123,10 +140,11 @@ private static class DatabaseOpenHelper extends SQLiteOpenHelper { "collection_size int, " + "etag string, " + "dirty string, " + - "timestamp string);"; + "timestamp string, " + + "fav int);"; private static final String ITEMS_CREATE = - "create table items"+ + "create table items"+ " (_id integer primary key autoincrement, " + "item_key string unique, " + "item_title string not null, " + @@ -140,7 +158,7 @@ private static class DatabaseOpenHelper extends SQLiteOpenHelper { "timestamp string);"; private static final String CREATORS_CREATE = - "create table creators"+ + "create table creators"+ " (_id integer primary key autoincrement, " + "name string, " + "firstName string, " + @@ -148,22 +166,22 @@ private static class DatabaseOpenHelper extends SQLiteOpenHelper { "creatorType string );"; private static final String ITEM_TO_CREATORS_CREATE = - "create table itemtocreators"+ + "create table itemtocreators"+ " (_id integer primary key autoincrement, " + "creator_id int not null, item_id int not null);"; private static final String ITEM_TO_COLLECTIONS_CREATE = - "create table itemtocollections"+ + "create table itemtocollections"+ " (_id integer primary key autoincrement, " + "collection_id int not null, item_id int not null);"; private static final String DELETED_ITEMS_CREATE = - "create table deleteditems"+ + "create table deleteditems"+ " (_id integer primary key autoincrement, " + "item_key string not null, etag string not null);"; private static final String ATTACHMENTS_CREATE = - "create table attachments"+ + "create table attachments"+ " (_id integer primary key autoincrement, " + "item_key string not null, " + "attachment_key string not null, " @@ -176,7 +194,7 @@ private static class DatabaseOpenHelper extends SQLiteOpenHelper { + "dirty string);"; private static final String APIREQUESTS_CREATE = - "create table apirequests"+ + "create table apirequests"+ " (_id integer primary key autoincrement, " + "uuid string unique, " + "type string, " @@ -194,7 +212,7 @@ private static class DatabaseOpenHelper extends SQLiteOpenHelper { /* We don't use this table right now */ private static final String NOTES_CREATE = - "create table notes"+ + "create table notes"+ " (_id integer primary key autoincrement, " + "item_key string, " + "note_key string not null, " @@ -204,6 +222,14 @@ private static class DatabaseOpenHelper extends SQLiteOpenHelper { + "status string, " + "content string, " + "etag string);"; + + // Note: Each pair has a Unique constraint + private static final String TAGS_CREATE = + "create table tags("+ + " _id integer primary key autoincrement, " + + "item_id int not null," + + "tag string not null," + + "UNIQUE(item_id, tag) ON CONFLICT REPLACE);"; DatabaseOpenHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); @@ -229,6 +255,7 @@ public void onCreate(SQLiteDatabase db) db.execSQL(ATTACHMENTS_CREATE); db.execSQL(NOTES_CREATE); db.execSQL(APIREQUESTS_CREATE); + db.execSQL(TAGS_CREATE); } @@ -254,7 +281,7 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, } if (oldVersion == 15 && newVersion > 15) { // here, we just added a table - db.execSQL("create table if not exists deleteditems"+ + db.execSQL("create table if not exists deleteditems"+ " (_id integer primary key autoincrement, " + "item_key int not null, etag int not null);"); } diff --git a/src/main/java/com/gimranov/zandy/app/data/Item.java b/src/main/java/com/mattrobertson/zotable/app/data/Item.java old mode 100644 new mode 100755 similarity index 90% rename from src/main/java/com/gimranov/zandy/app/data/Item.java rename to src/main/java/com/mattrobertson/zotable/app/data/Item.java index e6915ab..468da50 --- a/src/main/java/com/gimranov/zandy/app/data/Item.java +++ b/src/main/java/com/mattrobertson/zotable/app/data/Item.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Zandy is distributed in the hope that it will be useful, + * Zotable is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app.data; +package com.mattrobertson.zotable.app.data; import java.util.ArrayList; import java.util.Collections; @@ -30,8 +30,9 @@ import android.os.Bundle; import android.util.Log; -import com.gimranov.zandy.app.R; -import com.gimranov.zandy.app.task.APIRequest; + +import com.mattrobertson.zotable.app.R; +import com.mattrobertson.zotable.app.task.APIRequest; public class Item { private String id; @@ -56,9 +57,9 @@ public class Item { /** * Queue of dirty items to be sent to the server */ - public static ArrayList queue = new ArrayList(); + public static ArrayList dirtyQueue = new ArrayList(); - private static final String TAG = "com.gimranov.zandy.app.data.Item"; + private static final String TAG = "Item"; /** * The next two types are arrays of information on items that we need @@ -145,6 +146,24 @@ public void setTitle(String title) { } } + public void setDate(String date) { + if (date == null) + date = ""; + + this.content.remove("date"); + + try { + this.content.put("date", date); + + // TODO: strip year from date + + if (!APIRequest.API_CLEAN.equals(this.dirty)) + this.dirty = APIRequest.API_DIRTY; + } catch (JSONException e) { + Log.e(TAG, "Exception setting date", e); + } + } + /* * These can't be propagated, so they only make sense before the item has * been saved to the API. @@ -186,7 +205,7 @@ public JSONObject getContent() { } public void setContent(JSONObject content) { - if (!this.content.toString().equals(content.toString())) { + if (!this.content.toString().equals(content.toString())) { // not identical -- some update to the item has occurred if (!APIRequest.API_CLEAN.equals(this.dirty)) this.dirty = APIRequest.API_DIRTY; this.content = content; @@ -387,22 +406,14 @@ public int compare(Bundle b1, Bundle b2) { } /** - * Makes ArrayList from the present item's tags Primarily for use - * with TagActivity, but who knows? + * Makes ArrayList from the present item's tags */ public ArrayList tagsToBundleArray() { JSONObject itemContent = this.content; - /* - * Here we walk through the data and make Bundles to send to the - * ArrayAdapter. There should be no real risk of JSON exceptions, since - * the JSON was checked when initialized in the Item object. - * - * Each Bundle has three keys: "itemKey", "tag", and "type" - */ ArrayList rows = new ArrayList(); - Bundle b = new Bundle(); + Bundle b; if (!itemContent.has("tags")) { return rows; @@ -410,11 +421,11 @@ public ArrayList tagsToBundleArray() { try { JSONArray tags = itemContent.getJSONArray("tags"); + Log.d(TAG, tags.toString()); for (int i = 0; i < tags.length(); i++) { b = new Bundle(); - // Type is not always specified, but we try to get it - // and fall back to 0 when missing. + // Type is not always specified, but we try to get it and fall back to 0 when missing. Log.d(TAG, tags.getJSONObject(i).toString()); if (tags.getJSONObject(i).has("type")) b.putInt("type", tags.getJSONObject(i).optInt("type", 0)); @@ -429,6 +440,64 @@ public ArrayList tagsToBundleArray() { return rows; } + public boolean hasTag(String tag) { + ArrayList tags = tagsToBundleArray(); + + for (Bundle t : tags) { + if (t.getString("tag").equals(tag)) { + return true; + } + } + + return false; + } + + public void addTag(String tag) { + if (hasTag(tag)) + return; + + JSONObject itemContent = this.content; + + try { + // If there is no tags array for some reason, create one + if (!itemContent.has("tags")) + itemContent.put("tags", new JSONArray()); + + JSONArray jsoTags = itemContent.getJSONArray("tags"); + + JSONObject objTag = new JSONObject(); + objTag.put("tag",tag); + objTag.put("type","User"); // assuming tag is being added by user (as opposed to "Auto") + objTag.put("itemKey",this.key); + + jsoTags.put(objTag); + } + catch (JSONException e) { + Log.e(TAG, e.getMessage()); + } + } + + public void removeTag(String tag) { + if ( ! hasTag(tag)) + return; + + JSONObject itemContent = this.content; + + try { + JSONArray jsoTags = itemContent.getJSONArray("tags"); + + for (int i=0; i < jsoTags.length(); i++) { + if (jsoTags.getJSONObject(i).get("tag").equals(tag)) { + jsoTags.remove(i); + return; + } + } + } + catch (JSONException e) { + Log.e(TAG, e.getMessage()); + } + } + /** * Makes ArrayList from the present item's creators. Primarily for * use with CreatorActivity, but who knows? @@ -471,7 +540,7 @@ public ArrayList creatorsToBundleArray() { b.putString("lastName", creators.getJSONObject(i).optString( "lastName")); b.putString("name", creators.getJSONObject(i).optString( - "name")); + "name")); // If name is empty, fill with the others if (b.getString("name").equals("")) b.putString("name", b.getString("firstName") + " " @@ -491,20 +560,44 @@ public ArrayList creatorsToBundleArray() { * Saves the item's current state. Marking dirty should happen before this */ public void save(Database db) { + Item existing = load(key, db); - if (dbId == null && existing == null) { + + if (dbId == null && existing == null) { // Does Not Exist in db String[] args = { title, key, type, year, creatorSummary, content.toString(), etag, dirty, timestamp, children }; - Cursor cur = db - .rawQuery( - "insert into items (item_title, item_key, item_type, item_year, item_creator, item_content, etag, dirty, timestamp, item_children) " - + "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - args); + Cursor cur = db.rawQuery( + "insert into items (item_title, item_key, item_type, item_year, item_creator, item_content, etag, dirty, timestamp, item_children) " + + "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", args); if (cur != null) cur.close(); - Item fromDB = load(key, db); - dbId = fromDB.dbId; - } else { + Item itemFromDb = load(key, db); + dbId = itemFromDb.dbId; + + // Parse tags from JSON content & insert into db + try { + JSONArray jTagsArr = content.getJSONArray("tags"); + + Cursor curTags = null; + + for (int i=0; i atts = Attachment.forItem(this, db); for (Attachment a : atts) { a.delete(db); @@ -692,7 +809,7 @@ public static void setTag(String itemKey, String oldTag, String newTag, } item.content.put("tags", newTags); } catch (JSONException e) { - Log.e(TAG,"Caught JSON exception when we tried to modify the JSON content",e); + Log.e(TAG, "Caught JSON exception when we tried to modify the JSON content", e); } item.dirty = APIRequest.API_DIRTY; item.save(db); @@ -709,7 +826,7 @@ public static void setTag(String itemKey, String oldTag, String newTag, public static void setCreator(String itemKey, Creator c, int position, Database db) { // Load the item Item item = load(itemKey, db); - + try { JSONArray creators = item.content.getJSONArray("creators"); @@ -746,11 +863,11 @@ public static void setCreator(String itemKey, Creator c, int position, Database } /** - * Identifies dirty items in the database and queues them for syncing + * Identifies dirty (actually, non-clean) items in the database and queues them for syncing */ - public static void queue(Database db) { - Log.d(TAG, "Clearing item dirty queue before repopulation"); - queue.clear(); + public static void buildDirtyQueue(Database db) { + Log.d(TAG, "Clearing dirty-item queue before repopulation"); + dirtyQueue.clear(); Item item; String[] cols = Database.ITEMCOLS; String[] args = { APIRequest.API_CLEAN }; @@ -759,20 +876,29 @@ public static void queue(Database db) { if (cur == null) { Log.d(TAG, "No dirty items found in database"); - queue.clear(); + dirtyQueue.clear(); return; } do { - Log.d(TAG, "Adding item to dirty queue"); item = load(cur); - queue.add(item); + dirtyQueue.add(item); + Log.d(TAG, "Adding item to dirty queue: " + item.getTitle()); } while (cur.moveToNext() != false); if (cur != null) cur.close(); } + public static void setAllClean(Database db) { + Log.d(TAG, "Setting all items to sync-status=clean"); + dirtyQueue.clear(); + + String[] args = {APIRequest.API_CLEAN}; + + db.rawQuery("update items set dirty=?", args); + } + /** * Maps types to the resources providing images for them. * diff --git a/src/main/java/com/gimranov/zandy/app/data/ItemAdapter.java b/src/main/java/com/mattrobertson/zotable/app/data/ItemAdapter.java old mode 100644 new mode 100755 similarity index 86% rename from src/main/java/com/gimranov/zandy/app/data/ItemAdapter.java rename to src/main/java/com/mattrobertson/zotable/app/data/ItemAdapter.java index ce16e02..c8a7c6e --- a/src/main/java/com/gimranov/zandy/app/data/ItemAdapter.java +++ b/src/main/java/com/mattrobertson/zotable/app/data/ItemAdapter.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Zandy is distributed in the hope that it will be useful, + * Zotable is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app.data; +package com.mattrobertson.zotable.app.data; import android.content.Context; import android.database.Cursor; @@ -26,7 +26,8 @@ import android.widget.ResourceCursorAdapter; import android.widget.TextView; -import com.gimranov.zandy.app.R; +import com.mattrobertson.zotable.app.R; + /** * Exposes items to be displayed by a ListView @@ -34,7 +35,7 @@ * */ public class ItemAdapter extends ResourceCursorAdapter { - private static final String TAG = "com.gimranov.zandy.app.data.ItemAdapter"; + private static final String TAG = "ItemAdapter"; public ItemAdapter(Context context, Cursor cursor) { super(context, R.layout.list_item, cursor, false); diff --git a/src/main/java/com/gimranov/zandy/app/data/ItemCollection.java b/src/main/java/com/mattrobertson/zotable/app/data/ItemCollection.java old mode 100644 new mode 100755 similarity index 88% rename from src/main/java/com/gimranov/zandy/app/data/ItemCollection.java rename to src/main/java/com/mattrobertson/zotable/app/data/ItemCollection.java index b1db0f0..b100dfe --- a/src/main/java/com/gimranov/zandy/app/data/ItemCollection.java +++ b/src/main/java/com/mattrobertson/zotable/app/data/ItemCollection.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Zandy is distributed in the hope that it will be useful, + * Zotable is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app.data; +package com.mattrobertson.zotable.app.data; import java.util.ArrayList; import java.util.HashSet; @@ -24,7 +24,7 @@ import android.database.sqlite.SQLiteStatement; import android.util.Log; -import com.gimranov.zandy.app.task.APIRequest; +import com.mattrobertson.zotable.app.task.APIRequest; /** * Represents a Zotero collection of Item objects. Collections can @@ -43,7 +43,7 @@ public class ItemCollection extends HashSet { */ private static final long serialVersionUID = -4673800475017605707L; - private static final String TAG = "com.gimranov.zandy.app.data.ItemCollection"; + private static final String TAG = "ItemCollection"; /** * Queue of dirty collections to be sent to the server @@ -54,6 +54,7 @@ public class ItemCollection extends HashSet { private String title; private String key; private String etag; + private boolean isFav; /** * Subcollections of this collection. This is accessed through @@ -148,6 +149,14 @@ public void setTitle(String title) { this.dirty = APIRequest.API_DIRTY; } } + + public boolean isFavorite() { + return isFav; + } + + public void setFavorite(boolean fav) { + isFav = fav; + } /* I'm not sure how easy this is to propagate to the API */ public ItemCollection getParent(Database db) { @@ -225,7 +234,7 @@ public boolean add(Item item, boolean fromAPI, Database db) { } super.add(item); - Log.d(TAG, "Item added to collection"); + Log.d(TAG, "Item added to collection: " + item.getTitle() + " (" + getTitle() + ")"); if (!fromAPI) { Log.d(TAG, "Saving new collection membership request to database"); APIRequest req = APIRequest.add(item, this); @@ -260,21 +269,39 @@ public void save(Database db) { if (existing == null) { try { SQLiteStatement insert = db.compileStatement("insert or replace into collections " + - "(collection_name, collection_key, collection_parent, etag, dirty, collection_size, timestamp)" + - " values (?, ?, ?, ?, ?, ?, ?)"); + "(collection_name, collection_key, collection_parent, etag, dirty, collection_size, timestamp, fav)" + + " values (?, ?, ?, ?, ?, ?, ?, ?)"); // Why, oh why does bind* use 1-based indexing? And cur.get* uses 0-based! insert.bindString(1, title); - if (key == null) insert.bindNull(2); - else insert.bindString(2, key); - if (parentKey == null) insert.bindNull(3); - else insert.bindString(3, parentKey); - if (etag == null) insert.bindNull(4); - else insert.bindString(4, etag); - if (dirty == null) insert.bindNull(5); - else insert.bindString(5, dirty); + if (key == null) + insert.bindNull(2); + else + insert.bindString(2, key); + + if (parentKey == null) + insert.bindNull(3); + else + insert.bindString(3, parentKey); + + if (etag == null) + insert.bindNull(4); + else + insert.bindString(4, etag); + + if (dirty == null) + insert.bindNull(5); + else + insert.bindString(5, dirty); + insert.bindLong(6, size); - if (timestamp == null) insert.bindNull(7); - else insert.bindString(7, timestamp); + + if (timestamp == null) + insert.bindNull(7); + else + insert.bindString(7, timestamp); + + insert.bindLong(8, isFav ? 1 : 0); + insert.executeInsert(); insert.clearBindings(); insert.close(); @@ -294,17 +321,30 @@ public void save(Database db) { dbId = existing.dbId; try { SQLiteStatement update = db.compileStatement("update collections set " + - "collection_name=?, etag=?, dirty=?, collection_size=?, timestamp=?" + + "collection_name=?, etag=?, dirty=?, collection_size=?, timestamp=?, fav=?" + " where _id=?"); update.bindString(1, title); - if (etag == null) update.bindNull(2); - else update.bindString(2, etag); - if (dirty == null) update.bindNull(3); - else update.bindString(3, dirty); + if (etag == null) + update.bindNull(2); + else + update.bindString(2, etag); + + if (dirty == null) + update.bindNull(3); + else + update.bindString(3, dirty); + update.bindLong(4, size); - if (timestamp == null) update.bindNull(5); - else update.bindString(5, timestamp); - update.bindString(6, dbId); + + if (timestamp == null) + update.bindNull(5); + else + update.bindString(5, timestamp); + + update.bindLong(6, isFav ? 1 : 0); + + update.bindString(7, dbId); + update.executeInsert(); update.clearBindings(); update.close(); @@ -381,7 +421,7 @@ public void loadChildren(Database db) { String[] args = { dbId }; Cursor cursor = db.rawQuery("SELECT item_title, item_type, item_content, etag, dirty, items._id, item_key, item_year, item_creator, items.timestamp, item_children" + - " FROM items, itemtocollections WHERE items._id = item_id AND collection_id=? ORDER BY item_title", + " FROM items, itemtocollections WHERE items._id = item_id AND collection_id=? ORDER BY item_title", args); if (cursor != null) { cursor.moveToFirst(); @@ -442,7 +482,7 @@ public static ItemCollection load(String collKey, Database db) { if (collKey == null) return null; String[] cols = Database.COLLCOLS; String[] args = { collKey }; - Log.i(TAG, "Loading collection with key: "+collKey); + Log.i(TAG, "Loading collection with key: " + collKey); Cursor cur = db.query("collections", cols, "collection_key=?", args, null, null, null, null); ItemCollection coll = load(cur); @@ -467,7 +507,7 @@ public static ItemCollection load(Cursor cur) { if (cur == null) { return null; } - + coll.setTitle(cur.getString(0)); coll.setParent(cur.getString(1)); coll.etag = cur.getString(2); @@ -476,6 +516,8 @@ public static ItemCollection load(Cursor cur) { coll.setKey(cur.getString(5)); coll.size = cur.getInt(6); coll.timestamp = cur.getString(7); + coll.isFav = cur.getLong(8) == 1 ? true : false; + return coll; } @@ -483,7 +525,7 @@ public static ItemCollection load(Cursor cur) { * Identifies stale or missing collections in the database and queues them for syncing */ public static void queue(Database db) { - Log.d(TAG,"Clearing dirty queue before repopulation"); + Log.d(TAG,"Clearing dirty buildDirtyQueue before repopulation"); queue.clear(); ItemCollection coll; String[] cols = Database.COLLCOLS; @@ -497,7 +539,7 @@ public static void queue(Database db) { } do { - Log.d(TAG,"Adding collection to dirty queue"); + Log.d(TAG,"Adding collection to dirty buildDirtyQueue"); coll = load(cur); queue.add(coll); } while (cur.moveToNext() != false); @@ -541,7 +583,7 @@ public static ArrayList getCollections(Item i, Database db) { String[] args = { i.dbId }; Cursor cursor = db.rawQuery("SELECT collection_name, collection_parent," + " etag, dirty, collections._id, collection_key, collection_size," + - " timestamp FROM collections, itemtocollections" + + " timestamp, fav FROM collections, itemtocollections" + " WHERE collections._id = collection_id AND item_id=?" + " ORDER BY collection_name", args); diff --git a/src/main/java/com/gimranov/zandy/app/data/CollectionAdapter.java b/src/main/java/com/mattrobertson/zotable/app/data/TagAdapter.java old mode 100644 new mode 100755 similarity index 55% rename from src/main/java/com/gimranov/zandy/app/data/CollectionAdapter.java rename to src/main/java/com/mattrobertson/zotable/app/data/TagAdapter.java index 4972b7e..5216d73 --- a/src/main/java/com/gimranov/zandy/app/data/CollectionAdapter.java +++ b/src/main/java/com/mattrobertson/zotable/app/data/TagAdapter.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Zandy is distributed in the hope that it will be useful, + * Zotable is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app.data; +package com.mattrobertson.zotable.app.data; import android.content.Context; import android.database.Cursor; @@ -24,27 +24,27 @@ import android.widget.ResourceCursorAdapter; import android.widget.TextView; -import com.gimranov.zandy.app.R; -import com.gimranov.zandy.app.task.APIRequest; +import com.mattrobertson.zotable.app.R; +import com.mattrobertson.zotable.app.task.APIRequest; /** * Exposes collection to be displayed by a ListView * @author ajlyon * */ -public class CollectionAdapter extends ResourceCursorAdapter { - public static final String TAG = "com.gimranov.zandy.app.data.CollectionAdapter"; +public class TagAdapter extends ResourceCursorAdapter { + public static final String TAG = "TagAdapter"; public Context context; - - public CollectionAdapter(Context context, Cursor cursor) { - super(context, R.layout.list_collection, cursor, false); + + public TagAdapter(Context context, Cursor cursor) { + super(context, R.layout.list_tag, cursor, false); this.context = context; } public View newView(Context context, Cursor cur, ViewGroup parent) { LayoutInflater li = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - return li.inflate(R.layout.list_collection, parent, false); + return li.inflate(R.layout.list_tag, parent, false); } /** @@ -56,19 +56,19 @@ public void notifyDataSetChanged() { @Override public void bindView(View view, Context context, Cursor cursor) { - TextView tvTitle = (TextView)view.findViewById(R.id.collection_title); - TextView tvInfo = (TextView)view.findViewById(R.id.collection_info); - + TextView tvTag = (TextView)view.findViewById(R.id.tag_name); + String tagName = cursor.getString(2); + tvTag.setText(tagName); + Database db = new Database(context); - - ItemCollection collection = ItemCollection.load(cursor); - tvTitle.setText(collection.getTitle()); - StringBuilder sb = new StringBuilder(); - sb.append(collection.getSize() + " items"); - sb.append("; " + collection.getSubcollections(db).size() + " subcollections"); - if(!collection.dirty.equals(APIRequest.API_CLEAN)) - sb.append("; "+collection.dirty); - tvInfo.setText(sb.toString()); + + String[] args = {tagName}; + Cursor c = db.rawQuery("SELECT COUNT(*) FROM tags WHERE tag=?", args); + int itemCount = c.getInt(0); + + TextView tvItemCount = (TextView)view.findViewById(R.id.item_count); + tvItemCount.setText("Items: " + itemCount); + db.close(); } diff --git a/src/main/java/com/mattrobertson/zotable/app/task/APIEvent.java b/src/main/java/com/mattrobertson/zotable/app/task/APIEvent.java new file mode 100755 index 0000000..279b8af --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/task/APIEvent.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app.task; + +public interface APIEvent { + public void onComplete(APIRequest request); + + public void onUpdate(APIRequest request); + + public void onError(APIRequest request, Exception exception); + public void onError(APIRequest request, int error); +} diff --git a/src/main/java/com/gimranov/zandy/app/task/APIException.java b/src/main/java/com/mattrobertson/zotable/app/task/APIException.java old mode 100644 new mode 100755 similarity index 81% rename from src/main/java/com/gimranov/zandy/app/task/APIException.java rename to src/main/java/com/mattrobertson/zotable/app/task/APIException.java index 016054e..75f60a7 --- a/src/main/java/com/gimranov/zandy/app/task/APIException.java +++ b/src/main/java/com/mattrobertson/zotable/app/task/APIException.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Zandy is distributed in the hope that it will be useful, + * Zotable is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app.task; +package com.mattrobertson.zotable.app.task; public class APIException extends Exception { diff --git a/src/main/java/com/gimranov/zandy/app/task/APIRequest.java b/src/main/java/com/mattrobertson/zotable/app/task/APIRequest.java old mode 100644 new mode 100755 similarity index 97% rename from src/main/java/com/gimranov/zandy/app/task/APIRequest.java rename to src/main/java/com/mattrobertson/zotable/app/task/APIRequest.java index c84297d..b3233bf --- a/src/main/java/com/gimranov/zandy/app/task/APIRequest.java +++ b/src/main/java/com/mattrobertson/zotable/app/task/APIRequest.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Zandy is distributed in the hope that it will be useful, + * Zotable is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with Zandy. If not, see . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app.task; +package com.mattrobertson.zotable.app.task; import android.content.Context; import android.database.Cursor; @@ -22,12 +22,12 @@ import android.database.sqlite.SQLiteStatement; import android.os.Handler; import android.util.Log; -import com.gimranov.zandy.app.ServerCredentials; -import com.gimranov.zandy.app.XMLResponseParser; -import com.gimranov.zandy.app.data.Attachment; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.data.Item; -import com.gimranov.zandy.app.data.ItemCollection; +import com.mattrobertson.zotable.app.ServerCredentials; +import com.mattrobertson.zotable.app.XMLResponseParser; +import com.mattrobertson.zotable.app.data.Attachment; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemCollection; import org.apache.http.HttpEntity; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; @@ -67,7 +67,7 @@ * */ public class APIRequest { - private static final String TAG = "com.gimranov.zandy.app.task.APIRequest"; + private static final String TAG = "APIRequest"; /** * Statuses used for items and collections. They are currently strings, but @@ -627,12 +627,12 @@ public void issue(Database db, ServerCredentials cred) throws APIException { * ITEM_ATTACHMENT_UPDATE */ if ("xml".equals(disposition)) { + Log.i(TAG, "Using XMLResponseParser"); XMLResponseParser parse = new XMLResponseParser(this); // These types will always have a temporary key that we've // been using locally, and which should be replaced by the // incoming item key. - if (type == ITEM_NEW - || type == ITEM_ATTACHMENT_NEW) { + if (type == ITEM_NEW || type == ITEM_ATTACHMENT_NEW) { parse.update(updateType, updateKey); } @@ -647,8 +647,7 @@ public void issue(Database db, ServerCredentials cred) throws APIException { // shouldn't be anything else, so we throw in that case // for good measure if (!"get".equals(method)) { - throw new APIException(APIException.INVALID_METHOD, - "Unexpected method: "+method, this); + throw new APIException(APIException.INVALID_METHOD, "Unexpected method: "+method, this); } hr = client.execute(get); } @@ -732,6 +731,7 @@ public void issue(Database db, ServerCredentials cred) throws APIException { * supported; they should have a disposition of their own. */ else { + Log.i(TAG, "Using BasicResponseHandler"); BasicResponseHandler brh = new BasicResponseHandler(); String resp; diff --git a/src/main/java/com/gimranov/zandy/app/task/ZoteroAPITask.java b/src/main/java/com/mattrobertson/zotable/app/task/ZoteroAPITask.java old mode 100644 new mode 100755 similarity index 74% rename from src/main/java/com/gimranov/zandy/app/task/ZoteroAPITask.java rename to src/main/java/com/mattrobertson/zotable/app/task/ZoteroAPITask.java index 0a4e334..21796ab --- a/src/main/java/com/gimranov/zandy/app/task/ZoteroAPITask.java +++ b/src/main/java/com/mattrobertson/zotable/app/task/ZoteroAPITask.java @@ -1,25 +1,20 @@ -/* - * Zandy - * Based in part on Mendroid, Copyright 2011 Martin Paul Eve +/******************************************************************************* + * This file is part of Zotable. * - * This file is part of Zandy. + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * Zandy is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Zandy 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. + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. * - * You should have received a copy of the GNU General Public License - * along with Zandy. If not, see . - * - */ - -package com.gimranov.zandy.app.task; + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app.task; import java.util.ArrayList; @@ -29,11 +24,11 @@ import android.os.Message; import android.util.Log; -import com.gimranov.zandy.app.ServerCredentials; -import com.gimranov.zandy.app.XMLResponseParser; -import com.gimranov.zandy.app.data.Attachment; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.data.Item; +import com.mattrobertson.zotable.app.ServerCredentials; +import com.mattrobertson.zotable.app.XMLResponseParser; +import com.mattrobertson.zotable.app.data.Attachment; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; /** * Executes one or more API requests asynchronously. @@ -48,7 +43,7 @@ * */ public class ZoteroAPITask extends AsyncTask { - private static final String TAG = "com.gimranov.zandy.app.task.ZoteroAPITask"; + private static final String TAG = "ZoteroAPITask"; public ArrayList deletions; public ArrayList queue; @@ -122,16 +117,16 @@ public Message doFetch(APIRequest... reqs) return msg; } - // The XML parser's queue is simply from following continuations in the paged + // The XML parser's buildDirtyQueue is simply from following continuations in the paged // feed. We shouldn't split out its requests... if (XMLResponseParser.queue != null && !XMLResponseParser.queue.isEmpty()) { Log.i(TAG, "Finished call, but adding " + XMLResponseParser.queue.size() + - " items to queue."); + " items to buildDirtyQueue."); queue.addAll(XMLResponseParser.queue); XMLResponseParser.queue.clear(); } else { - Log.i(TAG, "Finished call, and parser's request queue is empty"); + Log.i(TAG, "Finished call, and parser's request buildDirtyQueue is empty"); } } @@ -144,7 +139,7 @@ public Message doFetch(APIRequest... reqs) ArrayList toRemove = new ArrayList(); for (APIRequest r : queue) { if (r.type != APIRequest.ITEMS_CHILDREN) { - Log.d(TAG, "Removing request from queue since last page had old items: "+r.query); + Log.d(TAG, "Removing request from buildDirtyQueue since last page had old items: "+r.query); toRemove.add(r); } } @@ -158,12 +153,12 @@ public Message doFetch(APIRequest... reqs) // XXX I suspect that this calling of doFetch from doFetch might be the cause of our // out-of-memory situations. We may be able to accomplish the same thing by expecting // the code listening to our handler to fetch again if QUEUED_MORE is received. In that - // case, we could just save our queue here and really return. + // case, we could just save our buildDirtyQueue here and really return. // XXX Test: Here, we try to use doInBackground instead doInBackground(requests); - // Return a message with the number of requests added to the queue + // Return a message with the number of requests added to the buildDirtyQueue Message msg = Message.obtain(); msg.arg1 = APIRequest.QUEUED_MORE; msg.arg2 = requests.length; @@ -180,13 +175,13 @@ public Message doFetch(APIRequest... reqs) } Log.d(TAG, "Sending local changes"); - Item.queue(db); + Item.buildDirtyQueue(db); Attachment.queue(db); APIRequest[] templ = {}; ArrayList list = new ArrayList(); - for (Item i : Item.queue) { + for (Item i : Item.dirtyQueue) { list.add(cred.prep(APIRequest.update(i))); } @@ -194,7 +189,7 @@ public Message doFetch(APIRequest... reqs) list.add(cred.prep(APIRequest.update(a, db))); } - // This queue has deletions, collection memberships, and failing requests + // This buildDirtyQueue has deletions, collection memberships, and failing requests // We may want to filter it in the future list.addAll(APIRequest.queue(db)); diff --git a/src/main/java/com/gimranov/zandy/app/webdav/WebDavTrust.java b/src/main/java/com/mattrobertson/zotable/app/webdav/WebDavTrust.java old mode 100644 new mode 100755 similarity index 60% rename from src/main/java/com/gimranov/zandy/app/webdav/WebDavTrust.java rename to src/main/java/com/mattrobertson/zotable/app/webdav/WebDavTrust.java index 61faafb..db5f7d7 --- a/src/main/java/com/gimranov/zandy/app/webdav/WebDavTrust.java +++ b/src/main/java/com/mattrobertson/zotable/app/webdav/WebDavTrust.java @@ -1,4 +1,20 @@ -package com.gimranov.zandy.app.webdav; +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Zotable is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app.webdav; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; diff --git a/src/main/java/org/pdfparse/PDFDefines.java b/src/main/java/org/pdfparse/PDFDefines.java new file mode 100755 index 0000000..91f8178 --- /dev/null +++ b/src/main/java/org/pdfparse/PDFDefines.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse; + + +public final class PDFDefines { + public static final boolean DEBUG = true; + public static final boolean PRETTY_PRINT = true; + public static final int MAX_SCAN_RANGE = 100; + +} diff --git a/src/main/java/org/pdfparse/cds/PDFRectangle.java b/src/main/java/org/pdfparse/cds/PDFRectangle.java new file mode 100755 index 0000000..0bac09c --- /dev/null +++ b/src/main/java/org/pdfparse/cds/PDFRectangle.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cds; + +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; +import org.pdfparse.cos.COSArray; +import org.pdfparse.cos.COSObject; +import org.pdfparse.exception.EParseError; + +import java.io.IOException; +import java.io.OutputStream; + +public class PDFRectangle implements COSObject { + /** lower left x */ + private float llx = 0; + + /** lower left y */ + private float lly = 0; + + /** upper right x */ + private float urx = 0; + + /** upper right y */ + private float ury = 0; + + // constructors + + /** + * Constructs a PdfRectangle-object. + * + * @param llx lower left x + * @param lly lower left y + * @param urx upper right x + * @param ury upper right y + * + */ + + public PDFRectangle(float llx, float lly, float urx, float ury) { + this.llx = llx; + this.lly = lly; + this.urx = urx; + this.ury = ury; + normalize(); + } + + public PDFRectangle(COSArray array) { + this.llx = array.getInt(0); + this.lly = array.getInt(1); + this.urx = array.getInt(2); + this.ury = array.getInt(3); + normalize(); + } + + @Override + public void parse(PDFRawData src, ParsingContext context) throws EParseError { + COSArray array = new COSArray(src, context); + this.llx = array.getInt(0); + this.lly = array.getInt(1); + this.urx = array.getInt(2); + this.ury = array.getInt(3); + normalize(); + } + + @Override + public void produce(OutputStream dst, ParsingContext context) throws IOException { + String s = String.format("[%.2f %.2f %.2f %.2f]", llx, lly, urx, ury); + dst.write(s.getBytes()); + } + + /** + * Return a string representation of this rectangle. + * + * @return This object as a string. + */ + public String toString() + { + return String.format("[%.2f %.2f %.2f %.2f]", llx, lly, urx, ury); + } + + public void normalize() { + float t; + if (llx > urx) { + t = llx; + llx = urx; + urx = t; + } + + if (lly > ury) { + t = lly; + lly = ury; + ury = t; + } + } + + /** + * Method to determine if the x/y point is inside this rectangle. + * @param x The x-coordinate to test. + * @param y The y-coordinate to test. + * @return True if the point is inside this rectangle. + */ + public boolean contains( float x, float y ) { + return x >= llx && x <= urx && + y >= lly && y <= ury; + } + + /** + * Get the width of this rectangle as calculated by + * upperRightX - lowerLeftX. + * + * @return The width of this rectangle. + */ + public float getWidth() { + return urx - llx; + } + + /** + * Get the height of this rectangle as calculated by + * upperRightY - lowerLeftY. + * + * @return The height of this rectangle. + */ + public float getHeight() { + return ury - lly; + } + + /** + * Move the rectangle the given relative amount. + * + * @param dx positive values will move rectangle to the right, negative's to the left. + * @param dy positive values will move the rectangle up, negative's down. + */ + public void move(float dx, float dy) { + llx += dx; + lly += dy; + urx += dx; + ury += dy; + } +} diff --git a/src/main/java/org/pdfparse/cos/COSArray.java b/src/main/java/org/pdfparse/cos/COSArray.java new file mode 100755 index 0000000..830c2a5 --- /dev/null +++ b/src/main/java/org/pdfparse/cos/COSArray.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cos; + +import org.pdfparse.exception.EParseError; +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; +import org.pdfparse.parser.PDFParser; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; + + +public class COSArray extends ArrayList implements COSObject { + + public COSArray() { + super(); + } + + public COSArray(PDFRawData src, ParsingContext context) throws EParseError { + super(); + parse(src, context); + } + + @Override + public void parse(PDFRawData src, ParsingContext context) throws EParseError { + src.pos++; // Skip '[' + src.skipWS(); + + while (src.pos < src.length) { + if (src.src[src.pos] == 0x5D) + break; // ']' + this.add( PDFParser.parseObject(src, context) ); + src.skipWS(); + } + if (src.pos == src.length) + return; + src.pos++; + src.skipWS(); + } + + @Override + public void produce(OutputStream dst, ParsingContext context) throws IOException { + dst.write(0x5B); // "[" + for (int i = 0; i < this.size(); i++) { + if (i != 0) { + if (i % 20 == 0) { + dst.write(0x0A); // "\n" + } else { + dst.write(0x20); // " "; + } + } + this.get(i).produce(dst, context); + } + dst.write(0x5D); // "]"; + } + + @Override + public String toString() { + return String.format("[ %d ]", this.size()); + } + + public int getInt(int idx) { + COSObject obj = get(idx); + if (obj instanceof COSNumber) return ((COSNumber)obj).intValue(); + else return 0; + } + + public float getFloat(int idx) { + COSObject obj = get(idx); + if (obj instanceof COSNumber) return ((COSNumber)obj).floatValue(); + else return 0; + } +} diff --git a/src/main/java/org/pdfparse/cos/COSBool.java b/src/main/java/org/pdfparse/cos/COSBool.java new file mode 100755 index 0000000..2b30cd0 --- /dev/null +++ b/src/main/java/org/pdfparse/cos/COSBool.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cos; + +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; + +import java.io.IOException; +import java.io.OutputStream; + +public class COSBool implements COSObject { + + static private final byte[] TRUE = {0x74, 0x72, 0x75, 0x65}; + static private final byte[] FALSE = {0x66, 0x61, 0x6c, 0x73, 0x65}; + public boolean value; + + public COSBool(Boolean val) { + value = val; + } + + @Override + public void parse(PDFRawData src, ParsingContext context) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void produce(OutputStream dst, ParsingContext context) throws IOException { + if (value) { + dst.write(TRUE); + } else { + dst.write(FALSE); + } + } + + @Override + public String toString() { + return String.valueOf(value); + } +} diff --git a/src/main/java/org/pdfparse/cos/COSDictionary.java b/src/main/java/org/pdfparse/cos/COSDictionary.java new file mode 100755 index 0000000..68e0e22 --- /dev/null +++ b/src/main/java/org/pdfparse/cos/COSDictionary.java @@ -0,0 +1,320 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cos; + +import org.pdfparse.*; +import org.pdfparse.cds.PDFRectangle; +import org.pdfparse.parser.PDFParser; +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; +import org.pdfparse.parser.ParsingGetObject; +import org.pdfparse.utils.DateConverter; +import org.pdfparse.exception.EParseError; + + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Calendar; +import java.util.LinkedHashMap; + +public class COSDictionary extends LinkedHashMap implements COSObject { + //private LinkedHashMap map; + private static final byte[] S_OPEN = {0x3C, 0x3C}; + private static final byte[] S_OPEN_PP = {0x3C, 0x3C, 0xA}; + private static final byte[] S_CLOSE = {0x3E, 0x3E}; + private static final byte[] S_CLOSE_PP = {0x3E, 0x3E, 0xA}; + private static final byte[] S_NULL = {0x6E, 0x75, 0x6C, 0x6C}; // "null" + + public COSDictionary() { + super(); + } + + public COSDictionary(COSDictionary src, ParsingContext context) { + super.putAll(src); + } + + public COSDictionary(PDFRawData src, ParsingContext context) throws EParseError { + parse(src, context); + } + + @Override + public void parse(PDFRawData src, ParsingContext context) throws EParseError { + //throw new UnsupportedOperationException("Not supported yet."); + src.pos +=2; + + while (src.pos < src.length) { + src.skipWS(); + if ((src.src[src.pos] == 0x3E)&&(src.src[src.pos+1] == 0x3E)) { // '>' + src.pos+=2; return; + } + + src.skipWS(); + COSName name = new COSName(src, context); + //if ((name.length == 0)||(name[0]!=0x2F)) throw new Error('This token is not a name: ' + name.toString()); // '/' + COSObject obj = PDFParser.parseObject(src, context); + this.put(name, obj); + } + throw new EParseError("Reach end of file while parsing dictionary"); + } + + @Override + public void produce(OutputStream dst, ParsingContext context) throws IOException { + COSObject obj; + if (PDFDefines.PRETTY_PRINT) + dst.write(S_OPEN_PP); // "<<\n" + else dst.write(S_OPEN); // "<<" + + for(COSName key: this.keySet()) { + key.produce(dst, context); + dst.write(0x20); + obj = this.get(key); + if (obj == null) + dst.write(S_NULL); + else + obj.produce(dst, context); + if (PDFDefines.PRETTY_PRINT) + dst.write(0xA); + } + + dst.write(S_CLOSE); + } + + @Override + public String toString() { + return String.format("<< %d >>", this.size()); + } + + private COSObject travel(COSObject obj, ParsingGetObject cache) throws EParseError { + int counter = 5; + while (obj instanceof COSReference) { + obj = cache.getObject((COSReference)obj); + if (counter-- == 0) + throw new EParseError("Infinite or too deep loop for " + obj.toString()); + } + return obj; + } + public static COSObject fetchValue(PDFRawData src) { + return null; + } + +// public void clear() { +// map.clear(); +// } + +// public boolean containsKey(String key) { +// return map.containsKey(key); +// } + +// public COSObject get(String key) { +// return map.get(key); +// } + +// public void set(String key, COSObject value) { +// map.put(key, value); +// } + + public boolean getBool(COSName name, boolean def_value) { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSBool) return ((COSBool)obj).value; + else return def_value; + } + + public boolean getBool(COSName name, ParsingGetObject cache, boolean def_value) throws EParseError { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSReference) + obj = travel(obj, cache); + if (obj instanceof COSBool) return ((COSBool)obj).value; + else return def_value; + } + + public void setBool(COSName name, boolean value) { + COSBool v = new COSBool(value); + this.put(name, v); + } + + public int getInt(COSName name, int def_value) { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSNumber) return ((COSNumber)obj).intValue(); + else return def_value; + } + public int getInt(COSName name, ParsingGetObject cache, int def_value) throws EParseError { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSReference) + obj = travel(obj, cache); + if (obj instanceof COSNumber) return ((COSNumber)obj).intValue(); + else return def_value; + } + public int getUInt(COSName name, int def_value) { + return getInt(name, def_value); + } + public int getUInt(COSName name, ParsingGetObject cache, int def_value) throws EParseError { + return getInt(name, cache, def_value); + } + + public void setInt(COSName name, int value) { + COSNumber v = new COSNumber(value); + this.put(name, v); + } + + public void setUInt(COSName name, int value) { + setInt(name, value); + } + + public String getStr(COSName name, String def_value) { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSString) return ((COSString)obj).getValue(); + else return def_value; + + } + + public String getStr(COSName name, ParsingGetObject cache, String def_value) throws EParseError { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSReference) + obj = travel(obj, cache); + if (obj == null) return def_value; + if (obj instanceof COSString) return ((COSString)obj).getValue(); + return def_value; + } + public void setStr(COSName name, String value) { + this.put(name, new COSString(value)); + } + + public void setDate(COSName name, Calendar date) { + setStr(name, DateConverter.toString(date)); + } + + public Calendar getDate(COSName name, Calendar def_value) throws EParseError { + String date = getStr(name, ""); + if (date.equals("")) return null; + return DateConverter.toCalendar( date ); + } + + public Calendar getDate(COSName name, ParsingGetObject cache, Calendar def_value) throws EParseError { + String date = getStr(name, cache, ""); + if (date.equals("")) return null; + return DateConverter.toCalendar( date ); + } + + public COSName getName(COSName name, COSName def_value) { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSName) return (COSName)obj; + else return def_value; + } + + public COSName getName(COSName name, ParsingGetObject cache, COSName def_value) throws EParseError { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSReference) + obj = travel(obj, cache); + if (obj instanceof COSName) return (COSName)obj; + else return def_value; + } + + public String getNameAsStr(COSName name, ParsingGetObject cache, String def_value) throws EParseError { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSReference) + obj = travel(obj, cache); + if (obj instanceof COSName) return ((COSName)obj).asString(); + else return def_value; + } + + public void setName(COSName name, COSName value) { + this.put(name, value); + } + + public COSArray getArray(COSName name, COSArray def_value) { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSArray) return (COSArray)obj; + else return def_value; + } + public COSArray getArray(COSName name, ParsingGetObject cache, COSArray def_value) throws EParseError { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSReference) + obj = travel(obj, cache); + if (obj instanceof COSArray) return (COSArray)obj; + else return def_value; + } + + public COSDictionary getDictionary(COSName name, COSDictionary def_value) { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSDictionary) return (COSDictionary)obj; + else return def_value; + } + public COSDictionary getDictionary(COSName name, ParsingGetObject cache, COSDictionary def_value) throws EParseError { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSReference) + obj = travel(obj, cache); + if (obj instanceof COSDictionary) return (COSDictionary)obj; + else return def_value; + } + + public byte[] getBlob(COSName name, byte[] def_value) { + throw new UnsupportedOperationException("Not supported yet."); + //COSObject obj = this.get(name); + //if (obj == null) return def_value; + //if (obj instanceof COSDictionary) return (COSDictionary)obj; + //else return def_value; + } + + public void setReference(COSName name, COSReference value) { + this.put(name, value); + } + + public void setReference(COSName name, int id, int gen) { + COSReference ref = new COSReference(id, gen); + this.put(name, ref); + } + + public COSReference getReference(COSName name) { + COSObject obj = this.get(name); + if (obj == null) return null; + if (obj instanceof COSReference) return (COSReference)obj; + else return null; + } + + public PDFRectangle getRectangle(COSName name) { + COSObject obj = this.get(name); + if (obj == null) return null; + if (obj instanceof COSArray) { + PDFRectangle rect = new PDFRectangle((COSArray)obj); + this.put(name, rect); // override existing COSArray with rectangle + return rect; + } + if (obj instanceof PDFRectangle) return (PDFRectangle)obj; + else return null; + } + + public void setRectangle(COSName name, PDFRectangle value) { + this.put(name, value); + } + +} diff --git a/src/main/java/org/pdfparse/cos/COSName.java b/src/main/java/org/pdfparse/cos/COSName.java new file mode 100755 index 0000000..fdfbc60 --- /dev/null +++ b/src/main/java/org/pdfparse/cos/COSName.java @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cos; + +import org.pdfparse.exception.EParseError; +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.Arrays; + + +public class COSName implements COSObject { + public static final COSName EMPTY = new COSName("/"); + public static final COSName UNKNOWN = new COSName("/Unknown"); + public static final COSName TRUE = new COSName("/True"); + public static final COSName FALSE = new COSName("/False"); + + public static final COSName PREV = new COSName("/Prev"); + public static final COSName XREFSTM = new COSName("/XRefStm"); + public static final COSName LENGTH = new COSName("/Length"); + public static final COSName TYPE = new COSName("/Type"); + public static final COSName XREF = new COSName("/XRef"); + public static final COSName W = new COSName("/W"); + public static final COSName SIZE = new COSName("/Size"); + public static final COSName INDEX = new COSName("/Index"); + public static final COSName FILTER = new COSName("/Filter"); + + public static final COSName FLATEDECODE = new COSName("/FlateDecode"); + public static final COSName FL = new COSName("/Fl"); + public static final COSName ASCIIHEXDECODE = new COSName("/ASCIIHexDecode"); + public static final COSName AHX = new COSName("/AHx"); + public static final COSName ASCII85DECODE = new COSName("/ASCII85Decode"); + public static final COSName A85 = new COSName("/A85"); + public static final COSName LZWDECODE = new COSName("/LZWDecode"); + public static final COSName CRYPT = new COSName("/Crypt"); + public static final COSName RUNLENGTHDECODE = new COSName("/RunLengthDecode"); + public static final COSName JPXDECODE = new COSName("/JPXDecode"); + public static final COSName CCITTFAXDECODE = new COSName("/CCITTFaxDecode"); + public static final COSName JBIG2DECODE = new COSName("/JBIG2Decode"); + + + + public static final COSName DCTDECODE = new COSName("/DCTDecode"); + public static final COSName ENCRYPT = new COSName("/Encrypt"); + public static final COSName DECODEPARMS = new COSName("/DecodeParms"); + public static final COSName PREDICTOR = new COSName("/Predictor"); + public static final COSName COLUMNS = new COSName("/Columns"); + public static final COSName COLORS = new COSName("/Colors"); + public static final COSName BITSPERCOMPONENT = new COSName("/BitsPerComponent"); + public static final COSName ROOT = new COSName("/Root"); + public static final COSName INFO = new COSName("/Info"); + public static final COSName ID = new COSName("/ID"); + + public static final COSName TITLE = new COSName("/Title"); + public static final COSName KEYWORDS = new COSName("/Keywords"); + public static final COSName SUBJECT = new COSName("/Subject"); + public static final COSName AUTHOR = new COSName("/Author"); + public static final COSName CREATOR = new COSName("/Creator"); + public static final COSName PRODUCER = new COSName("/Producer"); + public static final COSName CREATION_DATE = new COSName("/CreationDate"); + public static final COSName MOD_DATE = new COSName("/ModDate"); + public static final COSName TRAPPED = new COSName("/Trapped"); + + public static final COSName PAGES = new COSName("/Pages"); + public static final COSName METADATA = new COSName("/Metadata"); + public static final COSName COUNT = new COSName("/Count"); + public static final COSName CATALOG = new COSName("/Catalog"); + public static final COSName VERSION = new COSName("/Version"); + public static final COSName LANG = new COSName("/Lang"); + public static final COSName PAGELAYOUT = new COSName("/PageLayout"); + public static final COSName PAGEMODE = new COSName("/PageMode"); + + // A name object specifying the page layout shall be used when the document is opened: + public static final COSName PL_SINGLE_PAGE = new COSName("/SinglePage"); + public static final COSName PL_ONECOLUMN = new COSName("/OneColumn"); + public static final COSName PL_TWOCOLUMNLEFT = new COSName("/TwoColumnLeft"); + public static final COSName PL_TWOCOLUMNRIGHT = new COSName("/TwoColumnRight"); + public static final COSName PL_TWOPAGELEFT = new COSName("/TwoPageLeft"); + public static final COSName PL_TWOPAGERIGHT = new COSName("/TwoPageRight"); + + // A name object specifying how the document shall be displayed when opened: + public static final COSName PM_NONE = new COSName("/UseNone"); // Neither document outline nor thumbnail images visible + public static final COSName PM_OUTLINES = new COSName("/UseOutlines"); // Document outline visible + public static final COSName PM_THUMBS = new COSName("/UseThumbs"); // Thumbnail images visible + public static final COSName PM_FULLSCREEN = new COSName("/FullScreen"); // Full-screen mode, with no menu bar, window controls, or any other window visible + public static final COSName PM_OC = new COSName("/UseOC"); // (PDF 1.5) Optional content group panel visible + public static final COSName PM_ATTACHMENTS = new COSName("/UseAttachments"); // (PDF 1.6) Attachments panel visible + + + public static final COSName PARENT = new COSName("/PARENT"); + public static final COSName PAGE = new COSName("/PAGE"); + public static final COSName MEDIABOX = new COSName("/MediaBox"); + public static final COSName CROPBOX = new COSName("CropBox"); + public static final COSName KIDS = new COSName("Kids"); + + + + public static final COSName FIRST = new COSName("/First"); + public static final COSName N = new COSName("/N"); + + private static final int[] HEX = { // '0'..'f' + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15}; + + + + private byte[] value; + private int hc; + + public COSName(PDFRawData src, ParsingContext context) throws EParseError { + parse(src, context); + } + + public COSName(String val) { + value = val.getBytes(Charset.defaultCharset()); + hc = Arrays.hashCode(value); + } + + public String asString() { + return new String(value, Charset.defaultCharset()); + } + + @Override + public int hashCode() { + return hc; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) + return true; + + if (getClass() != obj.getClass()) { + return false; + } + final COSName other = (COSName) obj; + if ((this.hc != other.hc) || !Arrays.equals(this.value, other.value)) { + return false; + } + return true; + } + + @Override + public String toString() { + return new String(value, Charset.defaultCharset()); + } + + @Override + public void parse(PDFRawData src, ParsingContext context) throws EParseError { + src.skipWS(); + int p = src.pos; + int len = src.length; + int i, cnt = 0; + byte b, v1, v2; + boolean stop = false; + + if (src.src[src.pos] != 0x2F) + throw new EParseError("Expected SOLIDUS sign #2F in name object, but got " + Integer.toHexString(src.src[p])); + + p++; // skip '/' + + while ((p <= len) && !stop) { + b = src.src[p]; + context.softAssertFormatError(b >= 0, "Illegal character in name token"); + + switch (b) { + // Whitespace + case 0x00: + case 0x09: + case 0x0A: + case 0x0D: + case 0x20: + stop = true; + break; + + // Escape char + case 0x23: + cnt++; // escape char. skip it + break; + + // Delimeters + case 0x28: // ( - LEFT PARENTHESIS + case 0x29: // ) - RIGHT PARENTHESIS + case 0x3C: // < - LESS-THAN SIGN + case 0x3E: // > - GREATER-THAN SIGN + case 0x5B: // [ - LEFT SQUARE BRACKET + case 0x5D: // ] - RIGHT SQUARE BRACKET + case 0x7B: // { - LEFT CURLY BRACKET + case 0x7D: // } - RIGHT CURLY BRACKET + case 0x2F: // / - SOLIDUS + case 0x25: // % - PERCENT SIGN + stop = true; + break; + + default: + if ((b >= 0) && (b < 0x20)) + throw new EParseError("Illegal character in name token(2)"); + break; + } // switch ... + + if (stop) break; + p++; + } // while ... + + if (cnt == 0) { + value = new byte[p - src.pos]; + System.arraycopy(src.src, src.pos, value, 0, value.length); + src.pos = p; + hc = Arrays.hashCode(value); + return; + } + + value = new byte[p-src.pos - 2*cnt]; + cnt = 0; + for (i=src.pos; i>4)); + dst.write(0x30 + (value[i]&0xF)); + } else + dst.write(value[i]); + } + } +} diff --git a/src/main/java/org/pdfparse/cos/COSNull.java b/src/main/java/org/pdfparse/cos/COSNull.java new file mode 100755 index 0000000..a8cffa7 --- /dev/null +++ b/src/main/java/org/pdfparse/cos/COSNull.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cos; + +import org.pdfparse.exception.EParseError; +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; + +import java.io.IOException; +import java.io.OutputStream; + + +public class COSNull implements COSObject { + private static final byte[] S_NULL = {0x6E, 0x75, 0x6C, 0x6C}; // "null" + + @Override + public void parse(PDFRawData src, ParsingContext context) throws EParseError { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void produce(OutputStream dst, ParsingContext context) throws IOException { + dst.write(S_NULL); + } + + @Override + public String toString() { + return "null"; + } + +} diff --git a/src/main/java/org/pdfparse/cos/COSNumber.java b/src/main/java/org/pdfparse/cos/COSNumber.java new file mode 100755 index 0000000..4879b2b --- /dev/null +++ b/src/main/java/org/pdfparse/cos/COSNumber.java @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cos; + +import org.pdfparse.exception.*; +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * COSNumber provides two types of numbers, integer and real. + *

+ * Integers may be specified by signed or unsigned constants. Reals may only be + * in decimal format.
+ * This object is described in the 'Portable Document Format Reference Manual + * version 1.7' section 3.3.2 (page 52-53). + * + * @see COSObject + * @see EParseError + */ + +public final class COSNumber implements COSObject { + + /** + * actual value of this COSNumber, represented as a + * double + */ + private double value; + private boolean isInteger; + + public COSNumber(double val) { + value = val; + isInteger = false; + } + public COSNumber(float val) { + value = val; + isInteger = false; + } + public COSNumber(int val) { + value = val; + isInteger = true; + } + public COSNumber(long val) { + value = val; + isInteger = true; + } + + public COSNumber(PDFRawData src, ParsingContext context) { + parse(src, context); + } + + + /** + * {@inheritDoc} + */ + public boolean equals(Object o) + { + return o instanceof COSNumber && (((COSNumber)o).value == value); + // TODO: make decision about precision + // return o instanceof COSNumber && (Math.abs(((COSNumber)o).value - value) < 0.000001); + } + + /** + * {@inheritDoc} + */ + public int hashCode() + { + //taken from java.lang.Long + return Float.floatToIntBits((float)value); + } + + + /** + * Returns the primitive int value of this object. + * + * @return The value as int + */ + public int intValue() { + return (int) value; + } + + /** + * Returns the primitive long value of this object. + * + * @return The value as long + */ + public long longValue() { + return (long) value; + } + + /** + * Returns the primitive double value of this object. + * + * @return The value as double + */ + public double doubleValue() { + return value; + } + + /** + * Returns the primitive float value of this object. + * + * @return The value as float + */ + public float floatValue() { + return (float)value; + } + + + @Override + public void parse(PDFRawData src, ParsingContext context) throws EParseError { + int prev = src.pos; + float sign = 1; + float divider = 10; + + + boolean hasFractional = false; + isInteger = true; + value = 0; + + while (src.pos < src.length) { + switch (src.src[src.pos]) { + case 0x30: + case 0x31: + case 0x32: + case 0x33: + case 0x34: // 0..4 + case 0x35: + case 0x36: + case 0x37: + case 0x38: + case 0x39: // 5..9 + if (hasFractional) { + value += (src.src[src.pos] - 0x30) / divider; + divider *= 10; + } else + value = value * 10 + (src.src[src.pos] - 0x30); + src.pos++; + break; + case 0x2B: // + + if (src.pos == prev) { + sign = 1; + src.pos++; + break; + } + throw new EParseError("'+' not allowed here (invalid number)"); + case 0x2D: // - + if (src.pos == prev) { + sign = -1; + src.pos++; + break; + } + throw new EParseError("'-' not allowed here (invalid number)"); + case 0x2E: // . + if (hasFractional) + throw new EParseError("'.' not allowed here (invalid number)"); + hasFractional = true; + isInteger = false; + src.pos++; + break; + + // Separators + case 0x00: case 0x09: case 0x0A: case 0x0D: case 0x20: + // Delimeters + case 0x28: case 0x29: case 0x3C: case 0x3E: case 0x2F: + case 0x5B: case 0x5D: case 0x7B: case 0x7D: case 0x25: + if (prev == src.pos) + throw new EParseError("Number expected, got no value"); + + value = sign * value; + return; + default: + throw new EParseError("Number expected, got invalid value"); + } // switch + } // while + + if (prev == src.pos) + throw new EParseError("Number expected, got no value (2)"); + + value = sign * value; + return; + } + + @Override + public void produce(OutputStream dst, ParsingContext context) throws IOException { + dst.write(this.toString().getBytes()); + } + + @Override + public String toString() { + if (isInteger) + return String.valueOf((long)value); + else return String.format("%f.3", value); + } + + + static public void writeInteger(int val, OutputStream dst) throws IOException { + String str = Integer.toString(val); + for (int i = 0; i < str.length(); i++) { + dst.write(str.codePointAt(i)); + } + } + + static public int readInteger(PDFRawData src) throws EParseError { + int prev = src.pos; + int res = 0; + int sign = 1; + + while (src.pos < src.length) { + switch (src.src[src.pos]) { + case 0x30: + case 0x31: + case 0x32: + case 0x33: + case 0x34: // 0..4 + case 0x35: + case 0x36: + case 0x37: + case 0x38: + case 0x39: // 5..9 + res = res * 10 + (src.src[src.pos] - 0x30); + src.pos++; + break; + case 0x2B: // + + if (src.pos == prev) { + sign = 1; + src.pos++; + break; + } + throw new EParseError("Invalid integer value"); + case 0x2D: // - + if (src.pos == prev) { + sign = -1; + src.pos++; + break; + } + throw new EParseError("Invalid integer value"); + + // Separators + case 0x00: case 0x09: case 0x0A: case 0x0D: case 0x20: + // Delimeters + case 0x28: case 0x29: case 0x3C: case 0x3E: case 0x2F: + case 0x5B: case 0x5D: case 0x7B: case 0x7D: case 0x25: + if (prev == src.pos) + throw new EParseError("Number expected, got no value"); + + return sign * res; + + default: + throw new EParseError("Number expected, got invalid value (2)"); + } // switch + } // while + + if (prev == src.pos) + throw new EParseError("Number expected, got no value (3)"); + + return sign * res; + } + +} diff --git a/src/main/java/org/pdfparse/cos/COSObject.java b/src/main/java/org/pdfparse/cos/COSObject.java new file mode 100755 index 0000000..d08315c --- /dev/null +++ b/src/main/java/org/pdfparse/cos/COSObject.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cos; + +import org.pdfparse.exception.EParseError; +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; + +import java.io.OutputStream; +import java.io.IOException; + +public interface COSObject { + + public void parse(PDFRawData src, ParsingContext context) throws EParseError; + + public void produce(OutputStream dst, ParsingContext context) throws IOException; +} diff --git a/src/main/java/org/pdfparse/cos/COSReference.java b/src/main/java/org/pdfparse/cos/COSReference.java new file mode 100755 index 0000000..15b2a48 --- /dev/null +++ b/src/main/java/org/pdfparse/cos/COSReference.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cos; + +import org.pdfparse.exception.EParseError; +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; + + +public class COSReference implements COSObject { + + public int id; + public int gen; + + public COSReference(int id, int gen) { + this.id = id; + this.gen = gen; + } + + public void set(int id, int gen) { + this.id = id; + this.gen = gen; + } + + @Override + public void parse(PDFRawData src, ParsingContext context) throws EParseError { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void produce(OutputStream dst, ParsingContext context) throws IOException { + String s = String.format("%d %d R", id, gen); + dst.write(s.getBytes(Charset.defaultCharset())); + } + + @Override + public String toString() { + return String.format("%d %d R", id, gen); + } + +} diff --git a/src/main/java/org/pdfparse/cos/COSStream.java b/src/main/java/org/pdfparse/cos/COSStream.java new file mode 100755 index 0000000..453fe26 --- /dev/null +++ b/src/main/java/org/pdfparse/cos/COSStream.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cos; + +import org.pdfparse.exception.EParseError; +import org.pdfparse.parser.PDFParser; +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; + +import java.io.IOException; +import java.io.OutputStream; + + +public class COSStream extends COSDictionary { + private byte[] data = null; + + public COSStream(COSDictionary dict, PDFRawData src, ParsingContext context) throws EParseError { + super(dict, context); + + data = PDFParser.fetchStream(src, this.getUInt(COSName.LENGTH, context.objectCache, 0), true); + } + + @Override + public void parse(PDFRawData src, ParsingContext context) throws EParseError { + super.parse(src, context); + data = PDFParser.fetchStream(src, this.getUInt(COSName.LENGTH, context.objectCache,0), true); + } + @Override + public void produce(OutputStream dst, ParsingContext context) throws IOException { + //throw new ENotSupported(); + super.produce(dst, context); + //dst.write(null); + } + + public byte[] getData() { + return data; + } +} diff --git a/src/main/java/org/pdfparse/cos/COSString.java b/src/main/java/org/pdfparse/cos/COSString.java new file mode 100755 index 0000000..9e416e6 --- /dev/null +++ b/src/main/java/org/pdfparse/cos/COSString.java @@ -0,0 +1,585 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cos; + +import org.pdfparse.exception.EParseError; +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; +import org.pdfparse.utils.ByteBuffer; +import org.pdfparse.utils.IntIntHashtable; + +import java.io.IOException; +import java.io.OutputStream; + +public final class COSString implements COSObject { + + private static final byte[] C28 = {'\\', '('}; + private static final byte[] C29 = {'\\', ')'}; + private static final byte[] C5C = {'\\', '\\'}; + private static final byte[] C0A = {'\\', 'n'}; + private static final byte[] C0D = {'\\', 'r'}; + + static final char winansiByteToChar[] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, + 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, + 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, + 8364, 65533, 8218, 402, 8222, 8230, 8224, 8225, 710, 8240, 352, 8249, 338, 65533, 381, 65533, + 65533, 8216, 8217, 8220, 8221, 8226, 8211, 8212, 732, 8482, 353, 8250, 339, 65533, 382, 376, + 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, + 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, + 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, + 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, + 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, + 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255}; + static final IntIntHashtable winansi = new IntIntHashtable(); + + static final char pdfEncodingByteToChar[] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, + 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, + 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, + 0x2022, 0x2020, 0x2021, 0x2026, 0x2014, 0x2013, 0x0192, 0x2044, 0x2039, 0x203a, 0x2212, 0x2030, 0x201e, 0x201c, 0x201d, 0x2018, + 0x2019, 0x201a, 0x2122, 0xfb01, 0xfb02, 0x0141, 0x0152, 0x0160, 0x0178, 0x017d, 0x0131, 0x0142, 0x0153, 0x0161, 0x017e, 65533, + 0x20ac, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, + 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, + 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, + 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, + 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, + 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255}; + static final IntIntHashtable pdfEncoding = new IntIntHashtable(); + static { + for (int k = 128; k < 161; ++k) { + char c = winansiByteToChar[k]; + if (c != 65533) + winansi.put(c, k); + } + for (int k = 128; k < 161; ++k) { + char c = pdfEncodingByteToChar[k]; + if (c != 65533) + pdfEncoding.put(c, k); + } + } + + private static final int[] HEX2V = { // '0'..'f' + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15}; + private static final byte[] V2HEX = { // '0'..'f' + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x61, 0x62, 0x63, 0x64, 0x65, 0x66}; + + private static final byte[] EMPTY = {}; + + private String value; + private byte[] binaryValue; + private boolean forceHexForm; + + public COSString(String val) { + value = val; + binaryValue = val.getBytes(); + } + + public COSString(PDFRawData src, ParsingContext context) throws EParseError { + parse(src, context); + } + + public void clear() { + value = ""; + binaryValue = EMPTY; + } + + public String getValue() { + return value; + } + + public void setValue(String val) { + value = val; + binaryValue = convertToBytes(val, null); + } + + public byte[] getBinaryValue() { + return binaryValue; + } + + public void setBinaryValue(byte[] val) { + if (val == null) + binaryValue = EMPTY; + else binaryValue = val; + + value = convertToString(binaryValue); + } + + /** + * Forces the string to be written in literal form instead of hexadecimal form. + * + * @param v + * if v is true the string will be written in literal form, otherwise it will be written in hexa if + * necessary. + */ + + public void setForceLiteralForm(boolean v) + { + forceHexForm = !v; + } + + /** + * Forces the string to be written in hexadecimal form instead of literal form. + * + * @param v + * if v is true the string will be written in hexadecimal form otherwise it will be written in literal if + * necessary. + */ + + public void setForceHexForm(boolean v) + { + forceHexForm = v; + } + + @Override + public void parse(PDFRawData src, ParsingContext context) throws EParseError { + int nesting_brackets = 0; + int v; + byte ch; + value = ""; + binaryValue = EMPTY; + + if (src.src[src.pos] == '<') { + src.pos++; // Skip the opening bracket '<' + byte[] bytes = parseHexStream(src, context); + setBinaryValue(bytes); + forceHexForm = true; + return; + } + + // === this is a literal string + forceHexForm = false; + src.pos++; // Skip the opening bracket '(' + + ByteBuffer buffer = context.tmpBuffer; + buffer.reset(); + + while (src.pos < src.length) { + ch = src.src[src.pos]; + switch (ch) { + case 0x5C: // '\' + src.pos++; + if (src.pos >= src.length) + break; // finish. ignore this reverse solidus + + ch = src.src[src.pos]; + switch (ch) { + case 0x6E: // 'n' + buffer.append(0x0A); + break; + case 0x72: // 'r' + buffer.append(0x0D); + break; + case 0x74: // 't' + buffer.append(0x09); + break; + case 0x62: // 'b' + buffer.append(0x08); + break; + case 0x66: // 'f' + buffer.append(0x0C); + break; + case 0x28: // '(' + buffer.append(0x28); + break; + case 0x29: // ')' + buffer.append(0x29); + break; + case 0x5C: // '\' + buffer.append(0x5C); + break; + case 0x30: // '0'..'7' + case 0x31: + case 0x32: + case 0x33: + case 0x34: + case 0x35: + case 0x36: + case 0x37: + v = ch - 0x30; // convert first char to number + if ((src.src[src.pos + 1] >= 0x30) && (src.src[src.pos + 1] <= 0x37)) { + src.pos++; + v = v * 8 + (src.src[src.pos] - 0x30); + if ((src.src[src.pos + 1] >= 0x30) && (src.src[src.pos + 1] <= 0x37)) { + src.pos++; + v = v * 8 + (src.src[src.pos] - 0x30); + } + } + buffer.append(v); + break; + case 0x0A: + if ((src.pos < src.length) && (src.src[src.pos + 1] == 0x0D)) { + src.pos++; + } + break; + case 0x0D: + break; + + default: + // If the character following the REVERSE SOLIDUS is not one of those shown in Table 3, + // the REVERSE SOLIDUS shall be ignored. + buffer.append(src.src[src.pos]); //add this char + }//switch after '\' + + src.pos++; + break; + case 0x28: // '(' + nesting_brackets++; + buffer.append(0x28); + src.pos++; + break; + case 0x29: // ')' + nesting_brackets--; + if (nesting_brackets < 0) { //found closing bracket. End of string + src.pos++; + binaryValue = buffer.toByteArray(); + value = convertToString(binaryValue); + return; + } + buffer.append(0x29); + src.pos++; + break; + case 0x0D: // '\r': + case 0x0A: // '\n': + // An end-of-line marker appearing within a literal string without a preceding REVERSE SOLIDUS shall be treated + // as a byte value of (0Ah), irrespective of whether the end-of-line marker was a CARRIAGE RETURN (0Dh), a + // LINE FEED (0Ah), or both. + + buffer.append(0x0A); + src.pos++; + break; + default: + buffer.append(src.src[src.pos]); + src.pos++; + } // switch + } // while ... + + // Reach end-of-file/data? + if (src.pos < src.length) { + src.pos++; + } + + context.softAssertSyntaxComliance(nesting_brackets == 0, "Unbalanced brackets and illegal nesting while parsing string object"); + + binaryValue = buffer.toByteArray(); + value = convertToString(binaryValue); + } + + @Override + public void produce(OutputStream dst, ParsingContext context) throws IOException { + int i, j, len; + len = binaryValue.length; + + if (forceHexForm) { + // === Hexadecimal form + int b; + // TODO: use context.tmpBuffer + byte[] hex = new byte[binaryValue.length * 2]; + for (i = 0, j = 0; i < len; i++, j += 2) { + b = binaryValue[i] & 0xFF; + hex[j] = V2HEX[b >> 4]; + hex[j + 1] = V2HEX[b & 0xF]; + } + dst.write(0x3C); // "<" + dst.write(hex); + dst.write(0x3E); // ">" + return; + } + + // === Literal form + dst.write('('); + for (i = 0; i < len; i++) { + switch (binaryValue[i]) { + case 0x28: + dst.write(C28); + break; + case 0x29: + dst.write(C29); + break; + case 0x5C: + dst.write(C5C); + break; + case 0x0A: + dst.write(C0A); + break; + case 0x0D: + dst.write(C0D); + break; + + default: + dst.write(binaryValue[i]); + break; + } + } + dst.write(')'); + } + + @Override + public String toString() { + return value; + } + + + /** Converts a String to a byte array according + * to the font's encoding. + * @return an array of byte representing the conversion according to the font's encoding + * @param encoding the encoding + * @param text the String to be converted + */ + public static final byte[] convertToBytes(String text, String encoding) { + if (text == null) + return new byte[0]; + if (encoding == null || encoding.length() == 0) { + int len = text.length(); + byte b[] = new byte[len]; + for (int k = 0; k < len; ++k) + b[k] = (byte)text.charAt(k); + return b; + } + + return text.getBytes(); + /* + ExtraEncoding extra = extraEncodings.get(encoding.toLowerCase()); + if (extra != null) { + byte b[] = extra.charToByte(text, encoding); + if (b != null) + return b; + } + IntIntHashtable hash = null; + if (encoding.equals(BaseFont.WINANSI)) + hash = winansi; + else if (encoding.equals(PdfObject.TEXT_PDFDOCENCODING)) + hash = pdfEncoding; + if (hash != null) { + char cc[] = text.toCharArray(); + int len = cc.length; + int ptr = 0; + byte b[] = new byte[len]; + int c = 0; + for (int k = 0; k < len; ++k) { + char char1 = cc[k]; + if (char1 < 128 || char1 > 160 && char1 <= 255) + c = char1; + else + c = hash.get(char1); + if (c != 0) + b[ptr++] = (byte)c; + } + if (ptr == len) + return b; + byte b2[] = new byte[ptr]; + System.arraycopy(b, 0, b2, 0, ptr); + return b2; + } + if (encoding.equals(PdfObject.TEXT_UNICODE)) { + // workaround for jdk 1.2.2 bug + char cc[] = text.toCharArray(); + int len = cc.length; + byte b[] = new byte[cc.length * 2 + 2]; + b[0] = -2; + b[1] = -1; + int bptr = 2; + for (int k = 0; k < len; ++k) { + char c = cc[k]; + b[bptr++] = (byte)(c >> 8); + b[bptr++] = (byte)(c & 0xff); + } + return b; + } + try { + Charset cc = Charset.forName(encoding); + CharsetEncoder ce = cc.newEncoder(); + ce.onUnmappableCharacter(CodingErrorAction.IGNORE); + CharBuffer cb = CharBuffer.wrap(text.toCharArray()); + java.nio.ByteBuffer bb = ce.encode(cb); + bb.rewind(); + int lim = bb.limit(); + byte[] br = new byte[lim]; + bb.get(br); + return br; + } + catch (IOException e) { + throw new ExceptionConverter(e); + } */ + } + + + /** Converts a byte array to a String trying to detect encoding + * @param bytes the bytes to convert + * @return the converted String + */ + public static final String convertToString(byte bytes[], int offset, int length) { + if (bytes == null) + return ""; + // trying to detect encoding + if (bytes.length > 2 && ((bytes[0] & 0xFF) == 0xFE) && ((bytes[1] & 0xFF) == 0xFF)) { // UTF-16 BE + try { + return + new String(bytes, offset, length, "UTF-16"); + } catch (Exception e) {} + } + + if (offset + length > bytes.length) + length = bytes.length - offset; + if (length <= 0) + return ""; + + int dest = offset + length; + char c[] = new char[length]; + int i = 0; + + char[] map = pdfEncodingByteToChar; // use PDFEncoding + + for (int k = offset; k < dest; k++) + c[i++] = (char)map[bytes[k] & 0xff]; + return new String(c); + } + + public static final String convertToString(byte bytes[]) { + return convertToString(bytes, 0, bytes.length); + } + + public static final String convertToString(ByteBuffer bytes) { + return convertToString(bytes.getBuffer(), 0, bytes.size()); + } + + public static final String convertToString(byte bytes[], int offset, int length, String encoding) { + if (bytes == null) + return ""; + // trying to detect encoding + if (bytes.length > 2 && ((bytes[0] & 0xFF) == 0xFE) && ((bytes[1] & 0xFF) == 0xFF)) { // UTF-16 BE + try { + return + new String(bytes, offset, length, "UTF-16"); + } catch (Exception e) {} + } + + if (offset + length > bytes.length) + length = bytes.length - offset; + if (length <= 0) + return ""; + + int dest = offset + length; + char c[] = new char[length]; + int i = 0; + + char[] map; + if (encoding.equals("/WinAnsiEncoding")) + map = winansiByteToChar; + else + map = pdfEncodingByteToChar; // use PDFEncoding + + for (int k = offset; k < dest; k++) + c[i++] = (char)map[bytes[k] & 0xff]; + return new String(c); + } + /** Checks is text only has PdfDocEncoding characters. + * @param text the String to test + * @return true if only PdfDocEncoding characters are present + */ + public static boolean isPdfDocEncoding(String text) { + if (text == null) + return true; + int len = text.length(); + for (int k = 0; k < len; ++k) { + char char1 = text.charAt(k); + if (char1 < 128 || char1 > 160 && char1 <= 255) + continue; + if (!pdfEncoding.containsKey(char1)) + return false; + } + return true; + } + + + public static final byte[] parseHexStream(PDFRawData src, ParsingContext context) throws EParseError { + int ch, n, n1 = 0; + boolean first = true; + + //src.pos++; // Skip the opening bracket '<' + + ByteBuffer out = context.tmpBuffer; + out.reset(); + for (int i = src.pos; i < src.length; i++) { + ch = src.src[i] & 0xFF; + + if (ch == 0x3E) { // '>' - EOD + src.pos = i + 1; + if (!first) + out.append((byte)(n1 << 4)); + return out.toByteArray(); + } + // whitespace ? + if ((ch == 0x00) || (ch == 0x09) || (ch == 0x0A) || (ch == 0x0C) || (ch == 0x0D) || (ch == 0x20)) + continue; + + if ((ch < 0x30) || (ch > 0x66)) + throw new EParseError("Illegal character in hex string"); + + n = HEX2V[ch - 0x30]; + if (n < 0) + throw new EParseError("Illegal character in hex string"); + + if (first) + n1 = n; + else + out.append((byte)((n1 << 4) + n)); + first = !first; + } + + throw new EParseError("Unterminated hexadecimal string"); // ">" + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + + if (obj instanceof COSString) + { + COSString strObj = (COSString) obj; + return this.getValue().equals(strObj.getValue()) && this.forceHexForm == strObj.forceHexForm; + } + return false; + } + + @Override + public int hashCode() + { + int result = getValue().hashCode(); + return result += forceHexForm ? 17 : 0; + } + +} diff --git a/src/main/java/org/pdfparse/exception/EDateConvertError.java b/src/main/java/org/pdfparse/exception/EDateConvertError.java new file mode 100755 index 0000000..0e60f97 --- /dev/null +++ b/src/main/java/org/pdfparse/exception/EDateConvertError.java @@ -0,0 +1,48 @@ + +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.exception; + +import org.pdfparse.exception.EParseError; + +public class EDateConvertError extends EParseError { + + /** + * Creates a new instance of + * EParseError without detail message. + */ + public EDateConvertError() { + } + + /** + * Constructs an instance of + * EParseError with the specified detail message. + * + * @param msg the detail message. + */ + public EDateConvertError(String msg) { + super(msg); + } + + public EDateConvertError(String msg, Object... args) { + super(String.format(msg, args)); + } +} + diff --git a/src/main/java/org/pdfparse/exception/EDecoderException.java b/src/main/java/org/pdfparse/exception/EDecoderException.java new file mode 100755 index 0000000..be8ee08 --- /dev/null +++ b/src/main/java/org/pdfparse/exception/EDecoderException.java @@ -0,0 +1,45 @@ + +/* + * Copyright (c) 2014 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.exception; + +public class EDecoderException extends EParseError { + + public EDecoderException() { + super(); + } + + public EDecoderException(String message) { + super(message); + } + + public EDecoderException(Throwable cause) { + super(cause); + } + + public EDecoderException(String message, Throwable cause) { + super(message, cause); + } + + public EDecoderException(String msg, Object... args) { + super(String.format(msg, args)); + } + +} diff --git a/src/main/java/org/pdfparse/exception/ENotSupported.java b/src/main/java/org/pdfparse/exception/ENotSupported.java new file mode 100755 index 0000000..fd04890 --- /dev/null +++ b/src/main/java/org/pdfparse/exception/ENotSupported.java @@ -0,0 +1,44 @@ + +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.exception; + +import org.pdfparse.exception.EParseError; + +public class ENotSupported extends EParseError { + + /** + * Creates a new instance of + * EParseError without detail message. + */ + public ENotSupported() { + } + + /** + * Constructs an instance of + * EParseError with the specified detail message. + * + * @param msg the detail message. + */ + public ENotSupported(String msg) { + super(msg); + } +} + diff --git a/src/main/java/org/pdfparse/exception/EParseError.java b/src/main/java/org/pdfparse/exception/EParseError.java new file mode 100755 index 0000000..c78b325 --- /dev/null +++ b/src/main/java/org/pdfparse/exception/EParseError.java @@ -0,0 +1,56 @@ + +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.exception; + +public class EParseError extends RuntimeException { + + /** + * Creates a new instance of + * EParseError without detail message. + */ + public EParseError() { + super(); + } + + /** + * Constructs an instance of + * EParseError with the specified detail message. + * + * @param msg the detail message. + */ + public EParseError(String msg) { + super(msg); + } + + public EParseError(Throwable cause) { + super(cause); + } + + public EParseError(String message, Throwable cause) { + super(message, cause); + } + + public EParseError(String msg, Object... args) { + super(String.format(msg, args)); + } + + +} diff --git a/src/main/java/org/pdfparse/filter/LZWDecoder.java b/src/main/java/org/pdfparse/filter/LZWDecoder.java new file mode 100755 index 0000000..b819eb9 --- /dev/null +++ b/src/main/java/org/pdfparse/filter/LZWDecoder.java @@ -0,0 +1,242 @@ +/* + * Copyright 2002-2008 by Paulo Soares. + * + * This code was originally released in 2001 by SUN (see class + * com.sun.media.imageioimpl.plugins.tiff.TIFFLZWDecompressor.java) + * using the BSD license in a specific wording. In a mail dating from + * January 23, 2008, Brian Burkhalter (@sun.com) gave us permission + * to use the code under the following version of the BSD license: + * + * Copyright (c) 2005 Sun Microsystems, Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistribution of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistribution in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * Neither the name of Sun Microsystems, Inc. or the names of + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any + * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND + * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY + * EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL + * NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF + * USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR + * ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, + * CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND + * REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR + * INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed or intended for + * use in the design, construction, operation or maintenance of any + * nuclear facility. + */ +package org.pdfparse.filter; +import java.io.IOException; +import java.io.OutputStream; + +/** + * A class for performing LZW decoding. + * + * + */ +public class LZWDecoder { + + byte stringTable[][]; + byte data[] = null; + OutputStream uncompData; + int tableIndex, bitsToGet = 9; + int bytePointer, bitPointer; + int nextData = 0; + int nextBits = 0; + + int andTable[] = { + 511, + 1023, + 2047, + 4095 + }; + + public LZWDecoder() { + } + + /** + * Method to decode LZW compressed data. + * + * @param data The compressed data. + * @param uncompData Array to return the uncompressed data in. + */ + public void decode(byte data[], OutputStream uncompData) { + + if(data[0] == (byte)0x00 && data[1] == (byte)0x01) { + throw new RuntimeException("LZW flavour not supported"); + } + + initializeStringTable(); + + this.data = data; + this.uncompData = uncompData; + + // Initialize pointers + bytePointer = 0; + bitPointer = 0; + + nextData = 0; + nextBits = 0; + + int code, oldCode = 0; + byte string[]; + + while ((code = getNextCode()) != 257) { + + if (code == 256) { + + initializeStringTable(); + code = getNextCode(); + + if (code == 257) { + break; + } + + writeString(stringTable[code]); + oldCode = code; + + } else { + + if (code < tableIndex) { + + string = stringTable[code]; + + writeString(string); + addStringToTable(stringTable[oldCode], string[0]); + oldCode = code; + + } else { + + string = stringTable[oldCode]; + string = composeString(string, string[0]); + writeString(string); + addStringToTable(string); + oldCode = code; + } + } + } + } + + + /** + * Initialize the string table. + */ + public void initializeStringTable() { + + stringTable = new byte[8192][]; + + for (int i=0; i<256; i++) { + stringTable[i] = new byte[1]; + stringTable[i][0] = (byte)i; + } + + tableIndex = 258; + bitsToGet = 9; + } + + /** + * Write out the string just uncompressed. + */ + public void writeString(byte string[]) { + try { + uncompData.write(string); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Add a new string to the string table. + */ + public void addStringToTable(byte oldString[], byte newString) { + int length = oldString.length; + byte string[] = new byte[length + 1]; + System.arraycopy(oldString, 0, string, 0, length); + string[length] = newString; + + // Add this new String to the table + stringTable[tableIndex++] = string; + + if (tableIndex == 511) { + bitsToGet = 10; + } else if (tableIndex == 1023) { + bitsToGet = 11; + } else if (tableIndex == 2047) { + bitsToGet = 12; + } + } + + /** + * Add a new string to the string table. + */ + public void addStringToTable(byte string[]) { + + // Add this new String to the table + stringTable[tableIndex++] = string; + + if (tableIndex == 511) { + bitsToGet = 10; + } else if (tableIndex == 1023) { + bitsToGet = 11; + } else if (tableIndex == 2047) { + bitsToGet = 12; + } + } + + /** + * Append newString to the end of oldString. + */ + public byte[] composeString(byte oldString[], byte newString) { + int length = oldString.length; + byte string[] = new byte[length + 1]; + System.arraycopy(oldString, 0, string, 0, length); + string[length] = newString; + + return string; + } + + // Returns the next 9, 10, 11 or 12 bits + public int getNextCode() { + // Attempt to get the next code. The exception is caught to make + // this robust to cases wherein the EndOfInformation code has been + // omitted from a strip. Examples of such cases have been observed + // in practice. + try { + nextData = (nextData << 8) | (data[bytePointer++] & 0xff); + nextBits += 8; + + if (nextBits < bitsToGet) { + nextData = (nextData << 8) | (data[bytePointer++] & 0xff); + nextBits += 8; + } + + int code = + (nextData >> (nextBits - bitsToGet)) & andTable[bitsToGet-9]; + nextBits -= bitsToGet; + + return code; + } catch(ArrayIndexOutOfBoundsException e) { + // Strip not terminated as expected: return EndOfInformation code. + return 257; + } + } +} diff --git a/src/main/java/org/pdfparse/filter/StreamDecoder.java b/src/main/java/org/pdfparse/filter/StreamDecoder.java new file mode 100755 index 0000000..7666574 --- /dev/null +++ b/src/main/java/org/pdfparse/filter/StreamDecoder.java @@ -0,0 +1,480 @@ + +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.filter; + + +import org.pdfparse.exception.EDecoderException; +import org.pdfparse.parser.PDFParser; +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; +import org.pdfparse.cos.*; +import org.pdfparse.exception.EParseError; + +import java.io.ByteArrayOutputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +public class StreamDecoder { + + public static interface FilterHandler{ + public byte[] decode(byte[] b, COSName filterName, COSObject decodeParams, final COSDictionary streamDictionary, ParsingContext context) throws EParseError; + } + + private static final Map defaults; + static { + HashMap map = new HashMap(); + + map.put(COSName.FLATEDECODE, new Filter_FLATEDECODE()); + map.put(COSName.FL, new Filter_FLATEDECODE()); + map.put(COSName.ASCIIHEXDECODE, new Filter_ASCIIHEXDECODE()); + map.put(COSName.AHX, new Filter_ASCIIHEXDECODE()); + map.put(COSName.ASCII85DECODE, new Filter_ASCII85DECODE()); + map.put(COSName.A85, new Filter_ASCII85DECODE()); + map.put(COSName.LZWDECODE, new Filter_LZWDECODE()); + //map.put(COSName.CCITTFAXDECODE, new Filter_CCITTFAXDECODE()); + map.put(COSName.CRYPT, new Filter_DoNothing()); + map.put(COSName.RUNLENGTHDECODE, new Filter_RUNLENGTHDECODE()); + + // ignore this filters + map.put(COSName.DCTDECODE, new Filter_DoNothing()); + map.put(COSName.JPXDECODE, new Filter_DoNothing()); + map.put(COSName.CCITTFAXDECODE, new Filter_DoNothing()); + map.put(COSName.JBIG2DECODE, new Filter_DoNothing()); + + defaults = Collections.unmodifiableMap(map); + } + + + + public static byte[] FLATEDecode(final byte[] src) { + byte[] buf = new byte[1024]; + + Inflater decompressor = new Inflater(); + decompressor.setInput(src); + + // Create an expandable byte array to hold the decompressed data + ByteArrayOutputStream bos = new ByteArrayOutputStream(src.length); + + try { + while (!decompressor.finished()) { + int count = decompressor.inflate(buf); + bos.write(buf, 0, count); + } + } catch (DataFormatException e) { + decompressor.end(); + throw new EDecoderException("FlateDecode error", e); + } + decompressor.end(); + + return bos.toByteArray(); + } + + /** Decodes a stream that has the LZWDecode filter. + * @param in the input data + * @return the decoded data + */ + public static byte[] LZWDecode(final byte in[]) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + LZWDecoder lzw = new LZWDecoder(); + lzw.decode(in, out); + return out.toByteArray(); + } + + /** Decodes a stream that has the ASCIIHexDecode filter. + * @param in the input data + * @return the decoded data + */ + public static byte[] ASCIIHexDecode(final byte in[], ParsingContext context) throws EParseError { + PDFRawData data = new PDFRawData(); + data.src = in; + data.length = in.length; + data.pos = 0; + + return COSString.parseHexStream(data, context); + } + + /** Decodes a stream that has the ASCII85Decode filter. + * @param in the input data + * @return the decoded data + */ + public static byte[] ASCII85Decode(final byte in[]) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int state = 0; + int chn[] = new int[5]; + for (int k = 0; k < in.length; ++k) { + int ch = in[k] & 0xff; + if (ch == '~') + break; + + if (PDFRawData.isWhitespace(ch)) + continue; + if (ch == 'z' && state == 0) { + out.write(0); + out.write(0); + out.write(0); + out.write(0); + continue; + } + if (ch < '!' || ch > 'u') + throw new EDecoderException("Illegal character in ascii85decode (#%d)", ch); + chn[state] = ch - '!'; + ++state; + if (state == 5) { + state = 0; + int r = 0; + for (int j = 0; j < 5; ++j) + r = r * 85 + chn[j]; + out.write((byte)(r >> 24)); + out.write((byte)(r >> 16)); + out.write((byte)(r >> 8)); + out.write((byte)r); + } + } + int r = 0; + // We'll ignore the next two lines for the sake of perpetuating broken PDFs +// if (state == 1) +// throw new RuntimeException("illegal.length.in.ascii85decode"); + if (state == 2) { + r = chn[0] * 85 * 85 * 85 * 85 + chn[1] * 85 * 85 * 85 + 85 * 85 * 85 + 85 * 85 + 85; + out.write((byte)(r >> 24)); + } + else if (state == 3) { + r = chn[0] * 85 * 85 * 85 * 85 + chn[1] * 85 * 85 * 85 + chn[2] * 85 * 85 + 85 * 85 + 85; + out.write((byte)(r >> 24)); + out.write((byte)(r >> 16)); + } + else if (state == 4) { + r = chn[0] * 85 * 85 * 85 * 85 + chn[1] * 85 * 85 * 85 + chn[2] * 85 * 85 + chn[3] * 85 + 85; + out.write((byte)(r >> 24)); + out.write((byte)(r >> 16)); + out.write((byte)(r >> 8)); + } + return out.toByteArray(); + } + + public static PDFRawData decodeStream(byte[] src, COSDictionary dic, ParsingContext context) throws EParseError { + // Decompress stream + COSObject objFilter = dic.get(COSName.FILTER); + if (objFilter != null) { + COSArray filters = new COSArray(); + if (objFilter instanceof COSName) + filters.add((COSName)objFilter); + else if (objFilter instanceof COSArray) + filters.addAll((COSArray)objFilter); + + byte[] bytes = src; + for (int i=0; i bytesPerPixel, "Data to small for decoding PNG prediction") ) { + return in_out; + } + + + int filter = 0; + int curr_in_idx = 0, curr_out_idx = 0, prior_idx = 0; + // Decode the first line ------------------- + filter = in_out[curr_in_idx++]; + + switch (filter) { + case 0: //PNG_FILTER_NONE + case 2: //PNG_FILTER_UP + //curr[i] += prior[i]; + for (int i = 0; i < bytesPerRow; i++) + in_out[curr_out_idx + i] = in_out[curr_in_idx + i]; + break; + case 1: //PNG_FILTER_SUB + case 4: //PNG_FILTER_PAETH + //curr[i] += curr[i - bytesPerPixel]; + for (int i = 0; i < bytesPerPixel; i++) + in_out[curr_out_idx + i] = in_out[curr_in_idx + i]; + + for (int i = bytesPerPixel; i < bytesPerRow; i++) + in_out[curr_out_idx + i] = (byte)((in_out[curr_out_idx + i - bytesPerPixel]&0xff + in_out[curr_in_idx + i]&0xff)&0xff); + break; + case 3: //PNG_FILTER_AVERAGE + for (int i = 0; i < bytesPerPixel; i++) + in_out[curr_out_idx + i] = in_out[curr_in_idx + i]; + + for (int i = bytesPerPixel; i < bytesPerRow; i++) + in_out[curr_out_idx + i] = (byte) ((in_out[curr_in_idx + i - bytesPerPixel] & 0xff)/2); + break; + default: + // Error -- unknown filter type + throw new EDecoderException("PNG filter unknown (%d)", filter); + } + curr_in_idx += bytesPerRow; + curr_out_idx += bytesPerRow; + + //------------------------- + + + // Decode the (sub)image row-by-row + while (true) { + if (curr_in_idx >= in_out.length) + break; + + filter = in_out[curr_in_idx++]; + + switch (filter) { + case 0: //PNG_FILTER_NONE + for (int i = 0; i < bytesPerPixel; i++) + in_out[curr_out_idx + i] = in_out[curr_in_idx + i]; + break; + case 1: //PNG_FILTER_SUB + //curr[i] += curr[i - bytesPerPixel]; + for (int i = 0; i < bytesPerPixel; i++) + in_out[curr_out_idx + i] = in_out[curr_in_idx + i]; + + for (int i = bytesPerPixel; i < bytesPerRow; i++) + in_out[curr_out_idx + i] = (byte)(((in_out[curr_out_idx + i - bytesPerPixel]&0xff) + (in_out[curr_in_idx + i]&0xff))&0xff); + break; + case 2: //PNG_FILTER_UP + for (int i = 0; i < bytesPerRow; i++) { + //curr[i] += prior[i]; + in_out[curr_out_idx + i] = (byte) ((in_out[curr_in_idx + i]&0xff) + (in_out[prior_idx + i]&0xff)&0xff); + } + break; + case 3: //PNG_FILTER_AVERAGE + for (int i = 0; i < bytesPerPixel; i++) { + //curr[i] += prior[i] / 2; + in_out[curr_out_idx + i] += (byte) (((in_out[curr_in_idx + i]&0xff) + (in_out[prior_idx + i]&0xff) / 2)&0xff); + } + for (int i = bytesPerPixel; i < bytesPerRow; i++) { + //curr[i] += ((curr[i - bytesPerPixel] & 0xff) + (prior[i] & 0xff))/2; + in_out[curr_out_idx + i] += (byte) (( + (in_out[curr_out_idx + i - bytesPerPixel] & 0xff)+(in_out[prior_idx + i] & 0xff))/2); + } + break; + case 4: //PNG_FILTER_PAETH + for (int i = 0; i < bytesPerPixel; i++) { + //curr[i] += prior[i]; + in_out[curr_out_idx + i] = (byte) (((in_out[curr_in_idx + i]&0xff) + (in_out[prior_idx + i]&0xff)&0xff)); + } + + for (int i = bytesPerPixel; i < bytesPerRow; i++) { + //int a = curr[i - bytesPerPixel] & 0xff; + //int b = prior[i] & 0xff; + //int c = prior[i - bytesPerPixel] & 0xff; + int a = in_out[curr_out_idx + i - bytesPerPixel] & 0xFF; + int b = in_out[prior_idx + i] & 0xFF; + int c = in_out[prior_idx + i - bytesPerPixel] & 0xFF; + + int p = a + b - c; + int pa = Math.abs(p - a); + int pb = Math.abs(p - b); + int pc = Math.abs(p - c); + + int ret; + + if (pa <= pb && pa <= pc) { + ret = a; + } else if (pb <= pc) { + ret = b; + } else { + ret = c; + } + //curr[i] += (byte)ret; + in_out[curr_out_idx + i] += (byte)ret; + } + break; + default: + // Error -- unknown filter type + throw new EDecoderException("PNG filter unknown (%d)", filter); + } + + // Swap curr and prior + prior_idx = curr_out_idx; + curr_in_idx += bytesPerRow; + curr_out_idx += bytesPerRow; + } // while (true) ... + + byte[] res = new byte[curr_out_idx]; + System.arraycopy(in_out,0, res, 0, res.length); + return res; + } + + /** + * Handles FLATEDECODE filter + */ + private static class Filter_FLATEDECODE implements FilterHandler{ + public byte[] decode(byte[] b, COSName filterName, COSObject decodeParams, COSDictionary streamDictionary, ParsingContext context) throws EParseError { + b = StreamDecoder.FLATEDecode(b); + if (decodeParams != null) + b = StreamDecoder.decodePredictor(b, (COSDictionary)decodeParams, context); + return b; + } + } + + /** + * Handles ASCIIHEXDECODE filter + */ + private static class Filter_ASCIIHEXDECODE implements FilterHandler{ + public byte[] decode(byte[] b, COSName filterName, COSObject decodeParams, COSDictionary streamDictionary, ParsingContext context) throws EParseError { + b = StreamDecoder.ASCIIHexDecode(b, context); + return b; + } + } + + /** + * Handles ASCIIHEXDECODE filter + */ + private static class Filter_ASCII85DECODE implements FilterHandler{ + public byte[] decode(byte[] b, COSName filterName, COSObject decodeParams, COSDictionary streamDictionary, ParsingContext context) throws EParseError { + b = StreamDecoder.ASCII85Decode(b); + return b; + } + } + + /** + * Handles LZWDECODE filter + */ + private static class Filter_LZWDECODE implements FilterHandler{ + public byte[] decode(byte[] b, COSName filterName, COSObject decodeParams, COSDictionary streamDictionary, ParsingContext context) throws EParseError { + b = StreamDecoder.LZWDecode(b); + if (decodeParams != null) + b = StreamDecoder.decodePredictor(b, (COSDictionary)decodeParams, context); + return b; + } + } + + + /** + * A filter that doesn't modify the stream at all + */ + private static class Filter_DoNothing implements FilterHandler{ + public byte[] decode(byte[] b, COSName filterName, COSObject decodeParams, COSDictionary streamDictionary, ParsingContext context) throws EParseError { + return b; + } + } + + /** + * Handles RUNLENGTHDECODE filter + */ + private static class Filter_RUNLENGTHDECODE implements FilterHandler{ + + public byte[] decode(byte[] b, COSName filterName, COSObject decodeParams, COSDictionary streamDictionary, ParsingContext context) throws EParseError { + // allocate the output buffer + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte dupCount = -1; + for(int i = 0; i < b.length; i++){ + dupCount = b[i]; + if (dupCount == -128) break; // this is implicit end of data + + if (dupCount >= 0 && dupCount <= 127){ + int bytesToCopy = dupCount+1; + baos.write(b, i, bytesToCopy); + i+=bytesToCopy; + } else { + // make dupcount copies of the next byte + i++; + for(int j = 0; j < 1-(int)(dupCount);j++){ + baos.write(b[i]); + } + } + } + + return baos.toByteArray(); + } + } + +} diff --git a/src/main/java/org/pdfparse/filter/TIFFLZWDecoder.java b/src/main/java/org/pdfparse/filter/TIFFLZWDecoder.java new file mode 100755 index 0000000..dbd583f --- /dev/null +++ b/src/main/java/org/pdfparse/filter/TIFFLZWDecoder.java @@ -0,0 +1,273 @@ +/* + * Copyright 2003-2012 by Paulo Soares. + * + * This code was originally released in 2001 by SUN (see class + * com.sun.media.imageioimpl.plugins.tiff.TIFFLZWDecompressor.java) + * using the BSD license in a specific wording. In a mail dating from + * January 23, 2008, Brian Burkhalter (@sun.com) gave us permission + * to use the code under the following version of the BSD license: + * + * Copyright (c) 2005 Sun Microsystems, Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistribution of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistribution in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * Neither the name of Sun Microsystems, Inc. or the names of + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any + * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND + * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY + * EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL + * NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF + * USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR + * ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, + * CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND + * REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR + * INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed or intended for + * use in the design, construction, operation or maintenance of any + * nuclear facility. + * + * ---------- + * Modified & adapted by Anton Golinko'2013 + */ +package org.pdfparse.filter; + +/** + * A class for performing LZW decoding. + * + * + */ +public class TIFFLZWDecoder { + + byte stringTable[][]; + byte data[] = null, uncompData[]; + int tableIndex, bitsToGet = 9; + int bytePointer, bitPointer; + int dstIndex; + int w, h; + int predictor, samplesPerPixel; + int nextData = 0; + int nextBits = 0; + + int andTable[] = { + 511, + 1023, + 2047, + 4095 + }; + + public TIFFLZWDecoder(int w, int predictor, int samplesPerPixel) { + this.w = w; + this.predictor = predictor; + this.samplesPerPixel = samplesPerPixel; + } + + /** + * Method to decode LZW compressed data. + * + * @param data The compressed data. + * @param uncompData Array to return the uncompressed data in. + * @param h The number of rows the compressed data contains. + */ + public byte[] decode(byte data[], byte uncompData[], int h) { + + if(data[0] == (byte)0x00 && data[1] == (byte)0x01) { + throw new UnsupportedOperationException("TIFF 5.0 style lzw-codes are not supported"); + } + + initializeStringTable(); + + this.data = data; + this.h = h; + this.uncompData = uncompData; + + // Initialize pointers + bytePointer = 0; + bitPointer = 0; + dstIndex = 0; + + + nextData = 0; + nextBits = 0; + + int code, oldCode = 0; + byte string[]; + + while ( ((code = getNextCode()) != 257) && + dstIndex < uncompData.length) { + + if (code == 256) { + + initializeStringTable(); + code = getNextCode(); + + if (code == 257) { + break; + } + + writeString(stringTable[code]); + oldCode = code; + + } else { + + if (code < tableIndex) { + + string = stringTable[code]; + + writeString(string); + addStringToTable(stringTable[oldCode], string[0]); + oldCode = code; + + } else { + + string = stringTable[oldCode]; + string = composeString(string, string[0]); + writeString(string); + addStringToTable(string); + oldCode = code; + } + + } + + } + + // Horizontal Differencing Predictor + if (predictor == 2) { + + int count; + for (int j = 0; j < h; j++) { + + count = samplesPerPixel * (j * w + 1); + + for (int i = samplesPerPixel; i < w * samplesPerPixel; i++) { + + uncompData[count] += uncompData[count - samplesPerPixel]; + count++; + } + } + } + + return uncompData; + } + + + /** + * Initialize the string table. + */ + public void initializeStringTable() { + + stringTable = new byte[4096][]; + + for (int i=0; i<256; i++) { + stringTable[i] = new byte[1]; + stringTable[i][0] = (byte)i; + } + + tableIndex = 258; + bitsToGet = 9; + } + + /** + * Write out the string just uncompressed. + */ + public void writeString(byte string[]) { + // Fix for broken tiff files + int max = uncompData.length - dstIndex; + if (string.length < max) + max = string.length; + System.arraycopy(string, 0, uncompData, dstIndex, max); + dstIndex += max; + } + + /** + * Add a new string to the string table. + */ + public void addStringToTable(byte oldString[], byte newString) { + int length = oldString.length; + byte string[] = new byte[length + 1]; + System.arraycopy(oldString, 0, string, 0, length); + string[length] = newString; + + // Add this new String to the table + stringTable[tableIndex++] = string; + + if (tableIndex == 511) { + bitsToGet = 10; + } else if (tableIndex == 1023) { + bitsToGet = 11; + } else if (tableIndex == 2047) { + bitsToGet = 12; + } + } + + /** + * Add a new string to the string table. + */ + public void addStringToTable(byte string[]) { + + // Add this new String to the table + stringTable[tableIndex++] = string; + + if (tableIndex == 511) { + bitsToGet = 10; + } else if (tableIndex == 1023) { + bitsToGet = 11; + } else if (tableIndex == 2047) { + bitsToGet = 12; + } + } + + /** + * Append newString to the end of oldString. + */ + public byte[] composeString(byte oldString[], byte newString) { + int length = oldString.length; + byte string[] = new byte[length + 1]; + System.arraycopy(oldString, 0, string, 0, length); + string[length] = newString; + + return string; + } + + // Returns the next 9, 10, 11 or 12 bits + public int getNextCode() { + // Attempt to get the next code. The exception is caught to make + // this robust to cases wherein the EndOfInformation code has been + // omitted from a strip. Examples of such cases have been observed + // in practice. + try { + nextData = (nextData << 8) | (data[bytePointer++] & 0xff); + nextBits += 8; + + if (nextBits < bitsToGet) { + nextData = (nextData << 8) | (data[bytePointer++] & 0xff); + nextBits += 8; + } + + int code = + (nextData >> (nextBits - bitsToGet)) & andTable[bitsToGet-9]; + nextBits -= bitsToGet; + + return code; + } catch(ArrayIndexOutOfBoundsException e) { + // Strip not terminated as expected: return EndOfInformation code. + return 257; + } + } +} diff --git a/src/main/java/org/pdfparse/model/PDFDocCatalog.java b/src/main/java/org/pdfparse/model/PDFDocCatalog.java new file mode 100755 index 0000000..c55b175 --- /dev/null +++ b/src/main/java/org/pdfparse/model/PDFDocCatalog.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.model; + + +import org.pdfparse.cos.*; +import org.pdfparse.parser.ParsingContext; +import java.util.ArrayList; + +public class PDFDocCatalog { + private COSDictionary dRoot; + private COSDictionary dPages; + private ParsingContext context; + private ArrayList pages; + + public PDFDocCatalog(ParsingContext context, COSDictionary dic) { + dRoot = dic; + this.context = context; + + context.softAssertSyntaxComliance(COSName.CATALOG.equals(dic.getName(COSName.TYPE, null)), "Document catalog should be /Catalog type"); + } + + + public COSDictionary getCOSDictionary() { + return dRoot; + } + + /** + * Return the total page count of the PDF document. + * + * @return The total number of pages in the PDF document. + */ + public int getPagesCount() { + if (pages == null) { + COSReference refRootPages = dRoot.getReference(COSName.PAGES); + dPages = context.objectCache.getDictionary(refRootPages); + return dPages.getUInt(COSName.COUNT, context.objectCache, -1); + }; + + return pages.size(); + } + private void loadPage(COSReference cosReference) { + COSDictionary dict = context.objectCache.getDictionary(cosReference); + if (dict.getName(COSName.TYPE, COSName.EMPTY).equals(COSName.PAGES)) { + loadPages(dict); // This is a page node + return; + } + + this.pages.add( new PDFPage(dict) ); + } + + private void loadPages(COSDictionary pages) { + context.softAssertStructure( + pages.getName(COSName.TYPE, COSName.EMPTY).equals(COSName.PAGES), + "This dictionary should be /Type = /Pages"); + + + COSArray kids = pages.getArray(COSName.KIDS, context.objectCache, null); + if (!context.softAssertStructure(kids != null, "Required entry '/Kids' not found")) { + return; // will be zero pages + } + + for (int i=0; i getPages() { + if (pages != null) + return pages; + + getPagesCount(); + loadPages(dPages); + + if (pages == null) { + + } + + return pages; + } + + /** + * Returns the PDF specification version this document conforms to. + * + * @return The PDF version. + */ + public String getVersion() { + return dRoot.getNameAsStr(COSName.VERSION, context.objectCache, ""); + } + + /** Sets the PDF specification version this document conforms to. + * + * @param version the PDF version (ex. "1.4") + */ + public void setVersion(String version) { + dRoot.setName(COSName.VERSION, new COSName(version)); + } + + /** + * Get the metadata that is part of the document catalog. This will + * return null if there is no meta data for this object. + * + * @return The metadata for this object. + */ + public byte[] getXMLMetadata() { + COSReference refMetadata = dRoot.getReference(COSName.METADATA); + if (refMetadata == null) + return null; + COSStream dMetadata = context.objectCache.getStream(refMetadata); + if (dMetadata == null) + return null; + return dMetadata.getData(); + } + + /** + * The language for the document. + * + * @return The language for the document. + */ + public String getLanguage() { + return dRoot.getStr(COSName.LANG, context.objectCache, ""); + } + + /** + * Set the Language for the document. + * + * @param language The new document language. + */ + public void setLanguage( String language ) { + dRoot.setStr( COSName.LANG, language ); + } + + + /** + * Get the page layout, see the PL_XXX constants. + * @return A COSName representing the page layout. + */ + public COSName getPageLayout() { + return dRoot.getName( COSName.PAGELAYOUT, COSName.PL_SINGLE_PAGE ); + } + + /** + * Set the page layout, see the PL_XXX constants for valid values. + * @param layout The new page layout. + */ + public void setPageLayout( COSName layout ) { + dRoot.setName( COSName.PAGELAYOUT, layout ); + } + + /** + * Get the page display mode, see the PM_XXX constants. + * @return A COSName representing the page mode. + */ + public COSName getPageMode() { + return dRoot.getName( COSName.PAGEMODE, COSName.PM_NONE ); + } + + /** + * Set the page mode. See the PM_XXX constants for valid values. + * @param mode The new page mode. + */ + public void setPageMode( COSName mode ) { + dRoot.setName( COSName.PAGEMODE, mode ); + } + + + + + +} diff --git a/src/main/java/org/pdfparse/model/PDFDocInfo.java b/src/main/java/org/pdfparse/model/PDFDocInfo.java new file mode 100755 index 0000000..9700957 --- /dev/null +++ b/src/main/java/org/pdfparse/model/PDFDocInfo.java @@ -0,0 +1,304 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.model; + +import org.pdfparse.parser.PDFParser; +import org.pdfparse.cos.*; +import org.pdfparse.exception.EParseError; +import java.util.Calendar; + +/** + * This is the document metadata. Each getXXX method will return the entry if + * it exists or null if it does not exist. If you pass in null for the setXXX + * method then it will clear the value. + */ + +public class PDFDocInfo { + private COSDictionary info; + private PDFParser pdfParser; + private boolean owned; + + + /** + * Constructor that is used for a preexisting dictionary. + * + * @param dic The underlying dictionary. + * @param pdfParser Reference to the document parser object. + */ + public PDFDocInfo( COSDictionary dic, PDFParser pdfParser ) + { + if (dic == null) { + dic = new COSDictionary(); + owned = true; + } else owned = false; + + this.info = dic; + this.pdfParser = pdfParser; + } + + /** + * This will get the underlying dictionary that this object wraps. + * + * @return The underlying info dictionary. + */ + public COSDictionary getDictionary() + { + return info; + } + + /** + * This will get the title of the document. This will return null if no title exists. + * + * @return The title of the document. + * @throws EParseError If there is a problem retrieving the title + */ + public String getTitle() throws EParseError + { + return info.getStr(COSName.TITLE, pdfParser, ""); + } + + /** + * This will set the title of the document. + * + * @param title The new title for the document. + */ + public void setTitle( String title ) + { + info.setStr( COSName.TITLE, title ); + } + + /** + * This will get the author of the document. This will return null if no author exists. + * + * @return The author of the document. + * @throws EParseError If there is a problem retrieving the author + */ + public String getAuthor() throws EParseError + { + return info.getStr( COSName.AUTHOR, pdfParser, "" ); + } + + /** + * This will set the author of the document. + * + * @param author The new author for the document. + */ + public void setAuthor( String author ) + { + info.setStr( COSName.AUTHOR, author ); + } + + /** + * This will get the subject of the document. This will return null if no subject exists. + * + * @return The subject of the document. + * @throws EParseError If there is a problem retrieving the subject + */ + public String getSubject() throws EParseError + { + return info.getStr( COSName.SUBJECT, pdfParser, "" ); + } + + /** + * This will set the subject of the document. + * + * @param subject The new subject for the document. + */ + public void setSubject( String subject ) + { + info.setStr( COSName.SUBJECT, subject ); + } + + /** + * This will get the keywords of the document. This will return null if no keywords exists. + * + * @return The keywords of the document. + * @throws EParseError If there is a problem retrieving keywords + */ + public String getKeywords() throws EParseError + { + return info.getStr( COSName.KEYWORDS, pdfParser, "" ); + } + + /** + * This will set the keywords of the document. + * + * @param keywords The new keywords for the document. + */ + public void setKeywords( String keywords ) + { + info.setStr( COSName.KEYWORDS, keywords ); + } + + /** + * This will get the creator of the document. This will return null if no creator exists. + * + * @return The creator of the document. + * @throws EParseError If there is a problem retrieving the creator + */ + public String getCreator() throws EParseError + { + return info.getStr( COSName.CREATOR, pdfParser, "" ); + } + + /** + * This will set the creator of the document. + * + * @param creator The new creator for the document. + */ + public void setCreator( String creator ) + { + info.setStr( COSName.CREATOR, creator ); + } + + /** + * This will get the producer of the document. This will return null if no producer exists. + * + * @return The producer of the document. + * @throws EParseError If there is a problem retrieving the producer + */ + public String getProducer() throws EParseError + { + return info.getStr( COSName.PRODUCER, pdfParser, "" ); + } + + /** + * This will set the producer of the document. + * + * @param producer The new producer for the document. + */ + public void setProducer( String producer ) + { + info.setStr( COSName.PRODUCER, producer ); + } + + /** + * This will get the creation date of the document. This will return null if no creation date exists. + * + * @return The creation date of the document. + * + * @throws EParseError If there is an error creating the date. + */ + public Calendar getCreationDate() throws EParseError + { + return info.getDate( COSName.CREATION_DATE, pdfParser, null ); + } + + /** + * This will set the creation date of the document. + * + * @param date The new creation date for the document. + */ + public void setCreationDate( Calendar date ) + { + info.setDate( COSName.CREATION_DATE, date ); + } + + /** + * This will get the modification date of the document. This will return null if no modification date exists. + * + * @return The modification date of the document. + * + * @throws EParseError If there is an error creating the date. + */ + public Calendar getModificationDate() throws EParseError + { + return info.getDate( COSName.MOD_DATE, pdfParser, null ); + } + + /** + * This will set the modification date of the document. + * + * @param date The new modification date for the document. + */ + public void setModificationDate( Calendar date ) + { + info.setDate( COSName.MOD_DATE, date ); + } + + /** + * This will get the trapped value for the document. + * This will return COSName.UNKNOWN if one is not found. + * + * @return The trapped value for the document. + */ + public COSName getTrapped() + { + return info.getName(COSName.TRAPPED, COSName.UNKNOWN); + } + + /** + * This will get the keys of all metadata information fields for the document. + * + * @return all metadata key strings. + */ +// public Set getMetadataKeys() +// { +// Set keys = new TreeSet(); +// for (COSName key : info.keySet()) { +// keys.add(key.getName()); +// } +// return keys; +// } + + /** + * This will get the value of a custom metadata information field for the document. + * This will return null if one is not found. + * + * @param fieldName Name of custom metadata field from pdf document. + * + * @return String Value of metadata field + * + */ + public String getCustomMetadataValue(COSName fieldName) + { + return info.getStr( fieldName, "" ); + } + + /** + * Set the custom metadata value. + * + * @param fieldName The name of the custom metadata field. + * @param fieldValue The value to the custom metadata field. + */ + public void setCustomMetadataValue( COSName fieldName, String fieldValue ) + { + info.setStr( fieldName, fieldValue ); + } + + /** + * This will set the trapped of the document. This will be + * 'True', 'False', or 'Unknown'. + * + * @param value The new trapped value for the document. + */ + public void setTrapped( String value ) + { + if( value != null && + !value.equals( "True" ) && + !value.equals( "False" ) && + !value.equals( "Unknown" ) ) + { + throw new IllegalArgumentException( "Valid values for trapped are 'True', 'False', or 'Unknown'" ); + } + + info.setStr(COSName.TRAPPED, value); + } +} diff --git a/src/main/java/org/pdfparse/model/PDFDocument.java b/src/main/java/org/pdfparse/model/PDFDocument.java new file mode 100755 index 0000000..acf832a --- /dev/null +++ b/src/main/java/org/pdfparse/model/PDFDocument.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.model; + +import org.pdfparse.cos.*; +import org.pdfparse.exception.*; +import org.pdfparse.parser.*; +import org.pdfparse.parser.ParsingContext; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.FileChannel; + + +public class PDFDocument implements ParsingEvent { + private String filename; + private String filepath; + private boolean loaded; + + private ParsingContext context; + private PDFParser pdfParser; + + private COSReference rootID = null; + private COSReference infoID = null; + + private COSDictionary encryption = null; + + private PDFDocInfo documentInfo = null; + private PDFDocCatalog documentCatalog = null; + private byte[][] documentId = {null,null}; + private boolean documentIsEncrypted = false; + private float documentVersion = 0.0f; + + public PDFDocument() { + context = new ParsingContext(); + } + + public void close() { + pdfParser.done(); + loaded = false; + } + + public PDFDocument(String filename) throws EParseError, IOException { + this(); + File file = new File(filename); + open(file); + } + + public PDFDocument(File file) throws EParseError, IOException { + this(); + open(file); + } + + public PDFDocument(byte[] buffer) throws EParseError { + this(); + + this.filename = "internal"; + this.filepath = "internal"; + + open(buffer); + } + + private void open(File file) throws EParseError, IOException { + this.filename = file.getName(); + this.filepath = file.getParent(); + + FileInputStream fin = new FileInputStream(file); + FileChannel channel = fin.getChannel(); + + byte[] barray = new byte[(int) file.length()]; + ByteBuffer bb = ByteBuffer.wrap(barray); + bb.order(ByteOrder.BIG_ENDIAN); + channel.read(bb); + + open(barray); + } + + public void open(byte[] buffer) throws EParseError { + PDFRawData data = new PDFRawData(buffer); + pdfParser = new PDFParser(data, context, this); + loaded = true; + } + + /** + * Tell if this document is encrypted or not. + * + * @return true If this document is encrypted. + */ + public boolean isEncrypted() { + return documentIsEncrypted; + } + + public byte[][] getDocumentId() { + return documentId; + } + + /** + * Get the document info dictionary. This is guaranteed to not return null. + * + * @return The documents /Info dictionary + */ + public PDFDocInfo getDocumentInfo() throws EParseError { + if (documentInfo != null) + return documentInfo; + + COSDictionary dictInfo = null; + if (infoID != null) + dictInfo = pdfParser.getDictionary(infoID.id, infoID.gen, false); + + documentInfo = new PDFDocInfo(dictInfo, pdfParser); + return documentInfo; + } + + /** + * This will get the document CATALOG. This is guaranteed to not return null. + * + * @return The documents /Root dictionary + */ + public PDFDocCatalog getDocumentCatalog() throws EParseError { + if (documentCatalog == null) + { + COSDictionary dictRoot; + dictRoot = pdfParser.getDictionary(rootID, true); + + documentCatalog = new PDFDocCatalog(context, dictRoot); + } + return documentCatalog; + } + + public float getDocumentVersion() { + return documentVersion; + } + + @Override + public int onTrailerFound(COSDictionary trailer, int ordering) { + if (ordering == 0) { + rootID = trailer.getReference(COSName.ROOT); + infoID = trailer.getReference(COSName.INFO); + + documentIsEncrypted = trailer.containsKey(COSName.ENCRYPT); + + COSArray Ids = trailer.getArray(COSName.ID, null); + if (((Ids == null) || (Ids.size()!=2)) && documentIsEncrypted) + throw new EParseError("Missing (required) file identifier for encrypted document"); + + if (Ids != null) { + if (Ids.size() != 2) { + if (documentIsEncrypted) + throw new EParseError("Invalid document ID array size (should be 2)"); + context.softAssertSyntaxComliance(false, "Invalid document ID array size (should be 2)"); + + Ids = null; + } else { + if ((Ids.get(0) instanceof COSString) && (Ids.get(1) instanceof COSString)) { + documentId[0] = ((COSString)Ids.get(0)).getBinaryValue(); + documentId[1] = ((COSString)Ids.get(1)).getBinaryValue(); + } else context.softAssertSyntaxComliance(false, "Invalid document ID"); + } + } // Ids != null + } + return ParsingEvent.CONTINUE; + } + + @Override + public int onEncryptionDictFound(COSDictionary enc, int ordering) { + if (ordering == 0) + encryption = enc; + return ParsingEvent.CONTINUE; + } + + @Override + public int onNotSupported(String msg) { + //throw new UnsupportedOperationException("Not supported yet."); + return ParsingEvent.CONTINUE; + } + + @Override + public void onDocumentVersionFound(float version) { + this.documentVersion = version; + } + + public void dbgDump() { + //xref.dbgPrintAll(); + pdfParser.parseAndCacheAll(); + //cache.dbgSaveAllStreams(filepath + File.separator + "[" + filename + "]" ); + //cache.dbgSaveAllObjects(filepath + File.separator + "[" + filename + "]" ); + + } +} diff --git a/src/main/java/org/pdfparse/model/PDFPage.java b/src/main/java/org/pdfparse/model/PDFPage.java new file mode 100755 index 0000000..e2fb71d --- /dev/null +++ b/src/main/java/org/pdfparse/model/PDFPage.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.model; + +import org.pdfparse.cds.PDFRectangle; +import org.pdfparse.cos.COSDictionary; +import org.pdfparse.cos.COSName; + +/** + * This represents a single page in a PDF document. + * + *

+ * A Page object is a dictionary whose keys describe a single page containing text, + * graphics, and images. A Page object is a leaf of the Pages tree.
+ * This object is described in the 'Portable Document Format Reference Manual version 1.3' + * section 6.4 (page 73-81) + * + * @see PDFPageNode + */ + +public class PDFPage { + COSDictionary dPage; + + /** + * Creates a new instance of PDPage with a size of 8.5x11. + */ + public PDFPage() { + dPage = new COSDictionary(); + dPage.setName(COSName.TYPE, COSName.PAGE); + //setMediaBox(PAGE_SIZE_LETTER); + } + + /** + * Creates a new instance of PDPage. + * + * @param size The MediaBox or the page. + */ + public PDFPage(PDFRectangle size) + { + dPage = new COSDictionary(); + dPage.setName(COSName.TYPE, COSName.PAGE); + //setMediaBox(size); + } + + /** + * Creates a new instance of PDPage. + * + * @param pageDic The existing page dictionary. + */ + public PDFPage(COSDictionary pageDic) + { + dPage = pageDic; + } + + + /** + * This will get the underlying dictionary that this class acts on. + * + * @return The underlying dictionary for this class. + */ + public COSDictionary getCOSDictionary() { + return dPage; + } + + /** + * A rectangle, expressed in default user space units, defining the boundaries of the physical medium on which the + * page is intended to be displayed or printed + * + * This will get the MediaBox at this page and not look up the hierarchy. This attribute is inheritable, and + * findMediaBox() should probably used. This will return null if no MediaBox are available at this level. + * + * @return The MediaBox at this level in the hierarchy. + */ + public PDFRectangle getMediaBox() { + return dPage.getRectangle(COSName.MEDIABOX); + } + + /** + * Set the mediaBox for this page. + * + * @param value The new mediaBox for this page. + */ + public void setMediaBox(PDFRectangle value) { + if (value == null) { + dPage.remove(COSName.MEDIABOX); + } else { + dPage.setRectangle(COSName.MEDIABOX, value); + } + } + + /** + * A rectangle, expressed in default user space units, defining the visible region of default user space. When the + * page is displayed or printed, its contents are to be clipped (cropped) to this rectangle and then imposed on the + * output medium in some implementation-defined manner + * + * This will get the CropBox at this page and not look up the hierarchy. This attribute is inheritable, and + * findCropBox() should probably used. This will return null if no CropBox is available at this level. + * + * @return The CropBox at this level in the hierarchy. + */ + public PDFRectangle getCropBox() { + return dPage.getRectangle(COSName.CROPBOX); + } + + /** + * Set the CropBox for this page. + * + * @param value The new CropBox for this page. + */ + public void setCropBox(PDFRectangle value) { + if (value == null) { + dPage.remove(COSName.CROPBOX); + } else { + dPage.setRectangle(COSName.CROPBOX, value); + } + } + + + + +} \ No newline at end of file diff --git a/src/main/java/org/pdfparse/model/PDFPageNode.java b/src/main/java/org/pdfparse/model/PDFPageNode.java new file mode 100755 index 0000000..e3f8f1a --- /dev/null +++ b/src/main/java/org/pdfparse/model/PDFPageNode.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.model; + +import org.pdfparse.cos.COSDictionary; +import org.pdfparse.cos.COSName; +import org.pdfparse.cos.COSReference; + +/** + * This represents a page node in a pdf document. + *

+ * The Pages of a document are accessible through a tree of nodes known as the Pages tree. + * This tree defines the ordering of the pages in the document.
+ * This object is described in the 'Portable Document Format Reference Manual version 1.3' + * section 6.3 (page 71-73) + * + * @see org.pdfparse.model.PDFPage + */ + +public class PDFPageNode { + + private COSDictionary dPageNode; + + /** + * Creates a new instance of PDPage. + */ + public PDFPageNode() { + dPageNode = new COSDictionary(); + dPageNode.setName( COSName.TYPE, COSName.PAGES); + //page.setName(COSName.KIDS, new COSArray()); + dPageNode.setInt(COSName.COUNT, 0); + } + + /** + * Creates a new instance of PDPage. + * + * @param pages The dictionary pages. + */ + public PDFPageNode( COSDictionary pages ) { + dPageNode = pages; + } + + /** + * Get the count of descendent page objects. + * + * @return The total number of descendent page objects. + */ + public int getCount() { + if(dPageNode == null) + return 0; + + return dPageNode.getInt(COSName.COUNT, 0); + } + + /** + * This will get the underlying dictionary that this class acts on. + * + * @return The underlying dictionary for this class. + */ + public COSDictionary getCOSDictionary() { + return dPageNode; + } + + /** + * The parent page node. + * + * @return The parent to this page. + */ + public COSReference getParent() { + return dPageNode.getReference(COSName.PARENT); + } + + /** + * Set the parent of this page. + * + * @param parent The parent to this page node. + */ + public void setParent( COSReference parent ) { + dPageNode.setReference( COSName.PARENT, parent ); + } +} diff --git a/src/main/java/org/pdfparse/parser/PDFParser.java b/src/main/java/org/pdfparse/parser/PDFParser.java new file mode 100755 index 0000000..c1ebf6f --- /dev/null +++ b/src/main/java/org/pdfparse/parser/PDFParser.java @@ -0,0 +1,871 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.parser; + + +import org.pdfparse.PDFDefines; +import org.pdfparse.cos.*; +import org.pdfparse.exception.EParseError; +import org.pdfparse.filter.StreamDecoder; +import org.pdfparse.utils.IntObjHashtable; + +import java.util.Arrays; + +public class PDFParser implements ParsingGetObject { + private static final byte[] OBJ = {0x6F, 0x62, 0x6A}; + private static final byte[] ENDOBJ = {0x65, 0x6E, 0x64, 0x6F, 0x62, 0x6A}; + + private static final byte[] STREAM = {0x73, 0x74, 0x72, 0x65, 0x61, 0x6D}; + private static final byte[] ENDSTREAM = {0x65, 0x6E, 0x64, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D}; + + private static final byte[] PDF_HEADER = {0x25, 0x50, 0x44, 0x46, 0x2D}; // "%PDF-"; + private static final byte[] FDF_HEADER = {0x25, 0x46, 0x44, 0x46, 0x2D}; // "%FDF-"; + + private static final byte[] EOF = {0x25, 0x25, 0x45, 0x4F, 0x46}; // "%%EOF" + private static final byte[] STARTXREF = {0x73, 0x74, 0x61, 0x72, 0x74, 0x78, 0x72, 0x65, 0x66}; // "startxref" + + private static final byte[] XREF = {0x78, 0x72, 0x65, 0x66}; + private static final byte[] TRAILER = {0x74, 0x72, 0x61, 0x69, 0x6C, 0x65, 0x72}; + + private ParsingContext pContext; + private PDFRawData pdfData; + private ParsingEvent parsingEvent; + + private IntObjHashtable by_id; + + private int max_id = 0; + private int max_gen = 0; + private int max_offset = 0; + + private int compressed_max_stream_id = 0; + private int compressed_max_stream_offs = 0; + + public PDFParser(PDFRawData pData, ParsingContext pContext, ParsingEvent evt) { + this.pContext = pContext; + this.pdfData = pData; + this.pContext.objectCache = this; + this.parsingEvent = evt; + by_id = new IntObjHashtable(); + parse(); + } + + public void done() { + pContext = null; + by_id.clear(); + } + + public void clear() { + by_id.clear(); + max_id = 0; + max_gen = 0; + max_offset = 0; + compressed_max_stream_id = 0; + compressed_max_stream_offs = 0; + } + + /** + * Parse a PDF file data + * @param src PDF file data to parse + * @param evt Listener to broadcast parsing events + * @return Trailer dictionary + */ + private COSDictionary parse() { + PDFRawData src = pdfData; + + if (src.length < 10) { + throw new EParseError("This is not a valid PDF file"); + } + + // Check the PDF header & version ----------------------- + src.pos = 0; + if ( !( src.checkSignature(PDF_HEADER) || src.checkSignature(FDF_HEADER) ) ) { + if (!pContext.allowScan) + throw new EParseError("This is not a PDF file"); + + while ( !(src.checkSignature(PDF_HEADER) || src.checkSignature(FDF_HEADER)) + && (src.pos < pContext.headerLookupRange) && (src.pos < src.length) ) src.pos++; + + if ( !(src.checkSignature(PDF_HEADER) || src.checkSignature(FDF_HEADER)) ) + throw new EParseError("This is not a PDF file (PDF header not found)"); + } + + if (src.length - src.pos < 10) + throw new EParseError("This is not a valid PDF file"); + + + if ((src.src[src.pos + 5] != '1') || (src.src[src.pos + 7] < '1') || (src.src[src.pos + 7] > '8')) { + throw new EParseError("PDF version is not supported"); + } + + double documentVersion = (src.src[src.pos + 5] - '0') + (src.src[src.pos + 7] - '0') / 10.0; + parsingEvent.onDocumentVersionFound((float)documentVersion); + + + // Scan for EOF ----------------------------------------- + if (src.reverseScan(src.length, EOF, pContext.eofLookupRange) < 0) + throw new EParseError("Missing end of file marker"); + + // Scan for 'startxref' marker -------------------------- + if (src.reverseScan(src.pos, STARTXREF, 100) < 0) + throw new EParseError("Missing 'startxref' marker"); + + + // Fetch XREF offset ------------------------------------ + src.pos += 10; + src.skipWS(); + + int xref_offset = COSNumber.readInteger(src); + + if ((xref_offset == 0) || (xref_offset >= src.length)) { + throw new EParseError("Invalid xref offset"); + } + + src.pos = xref_offset; + + src.skipWS(); + if (src.checkSignature(XREF)) + return parseTableAndTrailer(src, parsingEvent); + return parseXRefStream(src, false, 0, parsingEvent); + } + + public XRefEntry getXRefEntry(int id, int gen) { + return by_id.get(id); + } + + public XRefEntry getXRefEntry(int id) { + return by_id.get(id); + } + +// public Set getIdSet() { +// return by_id.keySet(); +// } + + public COSObject getCOSObject(int id, int gen) throws EParseError { + COSReference header; + + XRefEntry x = by_id.get(id); // TODO: what with GEN ? + + if (x == null) + return new COSNull(); + + if (x.cachedObject != null) + return x.cachedObject; + + if (x.gen != gen) { + if (PDFDefines.DEBUG) + System.out.printf("Object with generation %d not found. But there is %d generation number", gen, x.gen); + } + + if (!x.isCompressed) { + pdfData.pos = x.fileOffset; + //----- + + header = this.tryFetchIndirectObjHeader(pdfData, pContext.tmpReference); + if (header == null) + throw new EParseError(String.format("Invalid indirect object header (expected '%d %d obj' @ %d)", id, gen, pdfData.pos)); + if ((header.id != id)||(header.gen != gen)) + throw new EParseError(String.format("Object header not correspond data specified in reference (expected '%d %d obj' @ %d)", id, gen, pdfData.pos)); + pdfData.skipWS(); + //----- + x.cachedObject = this.parseObject(pdfData, pContext); + return x.cachedObject; + } + + // Compressed ---------------------------------------------------- + XRefEntry cx = by_id.get(x.containerObjId); + if (cx == null) + return new COSNull(); + + if (cx.cachedObject == null) { // Extract compressed block (stream object) + pdfData.pos = cx.fileOffset; + //----- + header = this.tryFetchIndirectObjHeader(pdfData, pContext.tmpReference); + if (header == null) + throw new EParseError("Invalid indirect object header"); + if ((header.id != x.containerObjId)||(header.gen != 0)) + throw new EParseError("Object header not correspond data specified in reference"); + pdfData.skipWS(); + //----- + cx.cachedObject = this.parseObject(pdfData, pContext); + + if (! (cx.cachedObject instanceof COSStream)) + throw new EParseError("Referenced object-container is not stream object"); + } + + COSStream streamObject = (COSStream)cx.cachedObject; + + // --- Ok, received streamObject + // next, decompress its data, and put in cache + if (cx.decompressedStreamData == null) { + cx.decompressedStreamData = StreamDecoder.decodeStream(streamObject.getData(), streamObject, pContext); + } + PDFRawData streamData = cx.decompressedStreamData; + + // -- OK, retrieved from cache decompressed data + // Parse stream index & content + + int n = streamObject.getInt(COSName.N, 0); + int first = streamObject.getInt(COSName.FIRST, 0); + int idxId, idxOffset, savepos; + XRefEntry idxXRefEntry; + COSObject obj = null; + for (int i=0; i= len) return null; + + // parse int #1 -------------------------------------------- + ch = src.src[pos]; + while ((ch >= 0x30)&&(ch <= 0x39)) { + obj_id = obj_id*10 + (ch - 0x30); + pos++; // 0..9 + if (pos >= len) return null; + ch = src.src[pos]; + } + + //check if not a whitespace or EOF + if ((pos >= len)||(!((ch==0x20)||(ch==0x09)||(ch==0x0A)||(ch==0x0D)||(ch==0x00)))) + return null; + pos++; // skip this space + if (pos >= len) return null; + + // skip succeeded spaces if any + ch = src.src[pos]; + while ((ch==0x20)||(ch==0x09)||(ch==0x0A)||(ch==0x0D)) { + pos++; + if (pos >= len) return null; + ch = src.src[pos]; + } + + // parse int #2 -------------------------------------------- + while ((ch >= 0x30)&&(ch <= 0x39)) { + obj_gen = obj_gen*10 + (ch - 0x30); + pos++; + if (pos >= len) return null; + ch = src.src[pos]; + } + + //check if not a whitespace or EOF + if (!((ch==0x20)||(ch==0x09)||(ch==0x0A)||(ch==0x0D)||(ch==0x00))) + return null; + pos++; // skip space + if (pos >= len) return null; + + // skip succeeded spaces if any + ch = src.src[pos]; + while ((ch==0x20)||(ch==0x09)||(ch==0x0A)||(ch==0x0D)) { + pos++; + if (pos >= len) return null; + ch = src.src[pos]; + } + + // check if next char is R --------------------------------- + if (src.src[pos] != 0x52) // 'R' + return null; + + src.pos = ++pos; // beyond the 'R' + + return new COSReference(obj_id, obj_gen); + } + + // if next token is not a object header, function return null (without position changes) + // else it fetches token and change stream position + private static COSReference tryFetchIndirectObjHeader(PDFRawData src, COSReference outHeader) { + int pos = src.pos; + int len = src.length; + int ch; + + int obj_id = 0, obj_gen = 0; + + if (pos >= len) return null; + + // parse int #1 -------------------------------------------- + ch = src.src[pos]; + while ((ch >= 0x30)&&(ch <= 0x39)) { + obj_id = obj_id*10 + (ch - 0x30); + pos++; // 0..9 + if (pos >= len) return null; + ch = src.src[pos]; + } + + //check if not a whitespace or EOF + if ((!((ch==0x20)||(ch==0x09)||(ch==0x0A)||(ch==0x0D)||(ch==0x00)))) + return null; + pos++; // skip this space + if (pos >= len) return null; + + // skip succeeded spaces if any + ch = src.src[pos]; + while ((ch==0x20)||(ch==0x09)||(ch==0x0A)||(ch==0x0D)) { + pos++; + if (pos >= len) return null; + ch = src.src[pos]; + } + + // parse int #2 -------------------------------------------- + while ((ch >= 0x30)&&(ch <= 0x39)) { + obj_gen = obj_gen*10 + (ch - 0x30); + pos++; + if (pos >= len) return null; + ch = src.src[pos]; + } + + //check if not a whitespace or EOF + if ((!((ch==0x20)||(ch==0x09)||(ch==0x0A)||(ch==0x0D)||(ch==0x00)))) + return null; + pos++; // skip space + if (pos >= len) return null; + + // skip succeeded spaces if any + ch = src.src[pos]; + while ((ch==0x20)||(ch==0x09)||(ch==0x0A)||(ch==0x0D)) { + pos++; + if (pos >= len) return null; + ch = src.src[pos]; + } + + // check if next char is obj --------------------------------- + if (!src.checkSignature(pos, OBJ)) // 'obj' + return null; + + src.pos = pos + 3; // beyond the 'obj' + + outHeader.set(obj_id, obj_gen); + + return outHeader; + } + + public static final byte[] fetchStream(PDFRawData src, int stream_len, boolean movePosBeyoundEndObj) throws EParseError { + src.skipWS(); + if (!src.checkSignature(STREAM)) + throw new EParseError("'stream' keyword not found"); + src.pos += STREAM.length; + src.skipCRLForLF(); + if (src.pos + stream_len > src.length) + throw new EParseError("Unexpected end of file (stream object too large)"); + + // TODO: Lazy parse (reference + start + len) + byte[] res = Arrays.copyOfRange(src.src, src.pos, src.pos + stream_len); + src.pos += stream_len; + + if (movePosBeyoundEndObj) { + byte firstbyte = ENDOBJ[0]; + int max_pos = src.length - ENDOBJ.length; + if (max_pos - src.pos > PDFDefines.MAX_SCAN_RANGE) + max_pos = src.pos + PDFDefines.MAX_SCAN_RANGE; + for (int i = src.pos; i < max_pos; i++) + if ((src.src[i] == firstbyte)&&src.checkSignature(i, ENDOBJ)) { + src.pos = i + ENDOBJ.length; + return res; + } + + throw new EParseError("'endobj' tag not found"); + } + + return res; + } + + private void addXref(int id, int gen, int offs) throws EParseError { + // Skip invalid or not-used objects (assumed that they are free objects) + if (offs == 0) { + if (PDFDefines.DEBUG) + System.out.printf("XREF: Got object with zero offset. Assumed that this was a free object(%d %d R)\r\n", id, gen); + return; + } + if (offs < 0) + throw new EParseError("Negative offset for object id=%d", id); + + XRefEntry obj = new XRefEntry(); + obj.id = id; + obj.gen = gen; + obj.fileOffset = offs; + obj.isCompressed = false; + + XRefEntry old_obj = by_id.get(id); + + if (old_obj == null) { + by_id.put(id, obj); + } else if (old_obj.gen < gen) { + by_id.put(id, obj); + } + + if (max_id < id) max_id = id; + if (max_offset < offs) max_offset = offs; + if (max_gen < gen) max_gen = gen; + } + + private void addXrefCompressed(int id, int containerId, int indexWithinContainer) throws EParseError { + // Skip invalid or not-used objects (assumed that they are free objects) + if (containerId == 0) { + if (PDFDefines.DEBUG) + System.out.printf("XREF: Got containerId which is zero. Assumed that this was a free object (%d 0 R)\r\n", id); + return; + } + if (indexWithinContainer < 0) + throw new EParseError(String.format("Negative indexWithinContainer for compressed object id=%d in stream #%d", id, containerId)); + + XRefEntry obj = new XRefEntry(); + obj.id = id; + obj.gen = 0; + obj.fileOffset = 0; + obj.isCompressed = true; + obj.containerObjId = containerId; + obj.indexWithinContainer = indexWithinContainer; + + by_id.put(id, obj); + + if (compressed_max_stream_id 0x39)) break; // not in [0..9] range + }// while(1)... + } + + private COSDictionary parseTableAndTrailer(PDFRawData src, ParsingEvent evt) throws EParseError { + int prev = src.pos; + int xrefstrm = 0; + int res, trailer_ordering = 0; + COSDictionary curr_trailer = null; + COSDictionary dic_trailer = null; + + while (prev != 0) { + src.pos = prev; + // Parse XREF --------------------- + if (!src.checkSignature(XREF)) + throw new EParseError("This is not an 'xref' table"); + src.pos += XREF.length; + + parseTableOnly(src, false); + // Parse Trailer ------------------ + src.skipWS(); + if (!src.checkSignature(TRAILER)) + throw new EParseError("Cannot find 'trailer' tag"); + src.pos += TRAILER.length; + src.skipWS(); + + curr_trailer = new COSDictionary(src, pContext); + prev = curr_trailer.getInt(COSName.PREV, 0); + if (trailer_ordering == 0) + dic_trailer = curr_trailer; + + res = evt.onTrailerFound(curr_trailer, trailer_ordering); + if ((res & ParsingEvent.ABORT_PARSING) != 0) + return dic_trailer; + + // TODO: mark encrypted objects for removing + //----------------------- + if (trailer_ordering == 0) { + xrefstrm = curr_trailer.getInt(COSName.XREFSTM, 0); + if (xrefstrm != 0) { // This is an a hybrid PDF-file + //res = parsingEvent.onNotSupported("Hybrid PDF-files not supported"); + //if ((res&ParsingEvent.CONTINUE) == 0) + // return dic_trailer; + + src.pos = xrefstrm; + parseXRefStream(src, true, trailer_ordering+1, evt); + } + } + trailer_ordering++; + } // while + return dic_trailer; + } + + private COSDictionary parseXRefStream(PDFRawData src, boolean override, int trailer_ordering, ParsingEvent evt) throws EParseError { + COSDictionary curr_trailer, dic_trailer = null; + int res, prev; + while (true) { + src.skipWS(); + + COSReference x = PDFParser.tryFetchIndirectObjHeader(src, pContext.tmpReference); + if (x == null) + throw new EParseError("Invalid indirect object header"); + + src.skipWS(); + + + //addXRef(65530, 0, trailerOffset); + curr_trailer = new COSDictionary(src, pContext); + if (trailer_ordering == 0) + dic_trailer = curr_trailer; + + res = evt.onTrailerFound(curr_trailer, trailer_ordering); + if ((res & ParsingEvent.ABORT_PARSING) != 0) + return dic_trailer; + + // TODO: Mark 'encrypt' objects for removing + + if (!curr_trailer.getName(COSName.TYPE, null).equals(COSName.XREF)) + throw new EParseError("This is not a XRef stream"); + + + COSArray oW = curr_trailer.getArray(COSName.W, null); + if ((oW == null) || (oW.size() != 3)) + throw new EParseError("Invalid PDF file"); + int[] w = {oW.getInt(0), oW.getInt(1), oW.getInt(2)}; + + int size = curr_trailer.getUInt(COSName.SIZE, 0); + COSArray index = curr_trailer.getArray(COSName.INDEX, null); + if (index == null) { + index = new COSArray(); + index.add(new COSNumber(0)); + index.add(new COSNumber(size)); + } + + //int row_len = w[0] + w[1] + w[2]; + + //byte[] bstream = // TODO: implement max verbosity mode + // src.fetchStream(curr_trailer.getUInt(COSName.LENGTH, 0), false); + + PDFRawData bstream; + bstream = StreamDecoder.decodeStream(src, curr_trailer, pContext); + + int start; + int count; + int index_idx = 0; + + int itype, i2, i3; + + while (index_idx < index.size()) { + start = index.getInt(index_idx++); + count = index.getInt(index_idx++); + + int i = 0; + while (i < count) { + if (w[0] != 0) itype = bstream.fetchBinaryUInt(w[0]); else itype = 1; // default value (see specs) + if (w[1] != 0) i2 = bstream.fetchBinaryUInt(w[1]); else i2 = 0; + if (w[2] != 0) i3 = bstream.fetchBinaryUInt(w[2]); else i3 = 0; + + switch(itype) { + case 0: // linked list of free objects (corresponding to f entries in a cross-reference table). + i++; //TODO: mark as free (delete if exist) + continue; + case 1: // objects that are in use but are not compressed (corresponding to n entries in a cross-reference table). + addXref((start+i), i3, i2); + i++; + continue; + case 2: // compressed objects. + addXrefCompressed(start+i, i2, i3); + i++; + continue; + default: + //throw new EParseError("Invalid iType entry in xref stream"); + if (PDFDefines.DEBUG) + System.out.printf("Invalid iType entry in xref stream: %d\r\n", itype ); + continue; + }// switch + }// for + } // while + + prev = curr_trailer.getInt(COSName.PREV, 0); + if (prev != 0) { + if ((prev < 0) || (prev > src.length)) + throw new EParseError("Invalid trailer offset (%d)", prev); + src.pos = prev; + trailer_ordering++; + continue; + } else break; + } // while (true) + + return dic_trailer; + + } + + public void dbgPrintAll() { + System.out.printf("Max id: %d\r\n", max_id); + System.out.printf("Max gen: %d\r\n", max_gen); + System.out.printf("Max offset: %d\r\n", max_offset); + System.out.printf("Compressed max stream id: %d\r\n", compressed_max_stream_id); + System.out.printf("Compressed max stream offs: %d\r\n", compressed_max_stream_offs); + + XRefEntry xref; + int[] keys = by_id.getKeys(); + for (int i = 0; i length)) + throw new EParseError("Out of range"); // TODO: special exception + + int r = 0; + int b; + + b = src[pos++]; + r = (b & 0xFF); + if (size == 1) return r; + + b = src[pos++] & 0xFF; + r = (r<<8) | b; + if (size == 2) return r; + + b = src[pos++] & 0xFF; + r = (r<<8) | b; + if (size == 3) return r; + + b = src[pos++] & 0xFF; + r = (r<<8) | b; + if (size == 4) return r; + + throw + new EParseError("Invalid bytes length"); + + } + + public final boolean checkSignature(byte[] sign) { + int _to = this.pos + sign.length; + if (_to > this.length) return false; + for (int i = this.pos, j=0; i<_to; i++, j++) + if (this.src[i] != sign[j]) + return false; + return true; + } + + public final boolean checkSignature(int from, byte[] sign) { + int _to = from + sign.length; + if (_to > this.length) return false; + for (int i = from, j=0; i<_to; i++, j++) + if (this.src[i] != sign[j]) + return false; + return true; + } + + public final int reverseScan(int from, byte[] sign, int limit) { + pos = from - sign.length; + if (pos < 0) { + pos = 0; + return -1; + } + + int scanto = pos - limit; + if (scanto < 0) scanto = 0; + + boolean found; + + while (pos >= scanto) { + found = true; + for (int i = 0; i < sign.length; i++) + if (this.src[pos + i] != sign[i]) { + found = false; + break; + } + if (found) + return pos; + pos--; + } + pos = scanto; + return -1; + } + + public String dbgPrintBytes() { + int len = 90; + + if (this.pos+len > this.length) + len = this.length - this.pos; + + byte[] chunk = new byte[len]; + + System.arraycopy(src, pos, chunk, 0, len); + String s = ""; + + for (int i=0; i 0x19) + s += (char)chunk[i]; + else + s += "x"+ String.format("%02X", chunk[i]&0xFF); + + return s + " @ " + String.valueOf(pos); + } + + public String dbgPrintBytesBefore() { + int l = 20; + int r = 20; + + if (this.pos+r > this.length) + r = this.length - this.pos; + if (this.pos-l < 0) + l = this.pos; + + int len = r + l; + byte[] chunk = new byte[len]; + + System.arraycopy(src, pos - l, chunk, 0, len); + String s = ""; + + for (int i=0; i 0x19) + s += (char)chunk[i]; + else + s += "x"+ String.format("%02X", chunk[i]&0xFF); + if (i == l) s += "] "; + } + + return s + " @ " + String.valueOf(pos); + } +} diff --git a/src/main/java/org/pdfparse/parser/ParsingContext.java b/src/main/java/org/pdfparse/parser/ParsingContext.java new file mode 100755 index 0000000..0732885 --- /dev/null +++ b/src/main/java/org/pdfparse/parser/ParsingContext.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.parser; + +import org.pdfparse.cos.COSReference; +import org.pdfparse.exception.EParseError; +import org.pdfparse.utils.ByteBuffer; + +public class ParsingContext { + private boolean checkSyntaxCompliance = false; + private boolean ignoreErrors = false; + private boolean ignoreBasicSyntaxErrors = false; + + + public boolean allowScan = true; + public int headerLookupRange = 100; + public int eofLookupRange = 100; + + public ByteBuffer tmpBuffer = new ByteBuffer(1024); + public COSReference tmpReference = new COSReference(0, 0); + + public ParsingGetObject objectCache; + public boolean useEncryption; + public byte[] encryptionKey; + + public ParsingContext() { + + } + + public void checkAndLog(boolean canContinue, String message) { + if (canContinue) + System.err.println(message); + else + throw new EParseError(message); + } + + public boolean softAssertSyntaxComliance(boolean condition, String message) { + if (!condition) + checkAndLog(checkSyntaxCompliance, message); + return condition; + } + + public boolean softAssertFormatError(boolean condition, String message) { + if (!condition) + checkAndLog(ignoreBasicSyntaxErrors, message); + return condition; + } + + public boolean softAssertStructure(boolean condition, String message) { + if (!condition) + checkAndLog(ignoreErrors, message); + return condition; + } +} diff --git a/src/main/java/org/pdfparse/parser/ParsingEvent.java b/src/main/java/org/pdfparse/parser/ParsingEvent.java new file mode 100755 index 0000000..4ebfa05 --- /dev/null +++ b/src/main/java/org/pdfparse/parser/ParsingEvent.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.parser; + +import org.pdfparse.cos.COSDictionary; + +public interface ParsingEvent { + public final static int CONTINUE = 1; + public final static int ABORT_PARSING = 2; + + public int onTrailerFound(COSDictionary trailer, int ordering); + public int onEncryptionDictFound(COSDictionary enc, int ordering); + public int onNotSupported(String msg); + public void onDocumentVersionFound(float version); + +} diff --git a/src/main/java/org/pdfparse/parser/ParsingGetObject.java b/src/main/java/org/pdfparse/parser/ParsingGetObject.java new file mode 100755 index 0000000..a530041 --- /dev/null +++ b/src/main/java/org/pdfparse/parser/ParsingGetObject.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.parser; + +import org.pdfparse.cos.*; + +public interface ParsingGetObject { + public COSObject getObject (COSReference ref); + public COSDictionary getDictionary(COSReference ref); + public COSStream getStream(COSReference ref); + +} diff --git a/src/main/java/org/pdfparse/parser/XRefEntry.java b/src/main/java/org/pdfparse/parser/XRefEntry.java new file mode 100755 index 0000000..29ce571 --- /dev/null +++ b/src/main/java/org/pdfparse/parser/XRefEntry.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.parser; + +import org.pdfparse.cos.COSObject; + +public class XRefEntry { + public int id; + public int gen; + public int fileOffset; + public int containerObjId; + public int indexWithinContainer; + + public boolean isCompressed; + + public COSObject cachedObject; + public PDFRawData decompressedStreamData; + + @Override + public String toString() { + String s, name = ""; + if (cachedObject != null) + name = cachedObject.getClass().getName(); + + if (isCompressed) { + s = String.format("(%d %d R)/%s @ [%d + %d]", id, gen, name, containerObjId, indexWithinContainer); + } else { + s = String.format("(%d %d R)/%s @ %d", id, gen, name, fileOffset); + }; + return s; + } + + public byte[] getTextRef() { + String s = String.format("%d %d R", id, gen); + return s.getBytes(); + } +} diff --git a/src/main/java/org/pdfparse/utils/ByteBuffer.java b/src/main/java/org/pdfparse/utils/ByteBuffer.java new file mode 100755 index 0000000..038ddd4 --- /dev/null +++ b/src/main/java/org/pdfparse/utils/ByteBuffer.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.utils; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + + +/** + * Acts like a StringBuffer but works with byte arrays. + */ + +public class ByteBuffer extends OutputStream { + /** The count of bytes in the buffer. */ + protected int count; + + /** The buffer where the bytes are stored. */ + protected byte buf[]; + + /** Creates new ByteBuffer with capacity 128 */ + public ByteBuffer() { + this(128); + } + + /** + * Creates a byte buffer with a certain capacity. + * @param size the initial capacity + */ + public ByteBuffer(int size) { + if (size < 1) + size = 128; + buf = new byte[size]; + } + + + /** + * Appends an int. The size of the array will grow by one. + * @param b the int to be appended + * @return a reference to this ByteBuffer object + */ + public ByteBuffer append(int b) { + int newcount = count + 1; + if (newcount > buf.length) { + byte newbuf[] = new byte[Math.max(buf.length << 1, newcount)]; + System.arraycopy(buf, 0, newbuf, 0, count); + buf = newbuf; + } + buf[count] = (byte)b; + count = newcount; + return this; + } + + /** + * Appends the subarray of the byte array. The buffer will grow by + * len bytes. + * @param b the array to be appended + * @param off the offset to the start of the array + * @param len the length of bytes to append + * @return a reference to this ByteBuffer object + */ + public ByteBuffer append(byte b[], int off, int len) { + if ((off < 0) || (off > b.length) || (len < 0) || + ((off + len) > b.length) || ((off + len) < 0) || len == 0) + return this; + int newcount = count + len; + if (newcount > buf.length) { + byte newbuf[] = new byte[Math.max(buf.length << 1, newcount)]; + System.arraycopy(buf, 0, newbuf, 0, count); + buf = newbuf; + } + System.arraycopy(b, off, buf, count, len); + count = newcount; + return this; + } + + /** + * Appends an array of bytes. + * @param b the array to be appended + * @return a reference to this ByteBuffer object + */ + public ByteBuffer append(byte b[]) { + return append(b, 0, b.length); + } + + /** + * Appends a String to the buffer. The String is + * converted platform's default charset. + * @param str the String to be appended + * @return a reference to this ByteBuffer object + */ + public ByteBuffer append(String str) { + if (str != null) + return append(str.getBytes()); + return this; + } + + /** + * Appends a char to the buffer. The char is + * converted according to the encoding ISO-8859-1. + * @param c the char to be appended + * @return a reference to this ByteBuffer object + */ + public ByteBuffer append(char c) { + return append((int)c); + } + + /** + * Appends another ByteBuffer to this buffer. + * @param buf the ByteBuffer to be appended + * @return a reference to this ByteBuffer object + */ + public ByteBuffer append(ByteBuffer buf) { + return append(buf.buf, 0, buf.count); + } + + /** + * Sets the size to zero. + */ + public void reset() { + count = 0; + } + + /** + * Clear memory + */ + public void clear() { + count = 0; + buf = new byte[128]; + } + + /** + * Creates a newly allocated byte array. Its size is the current + * size of this output stream and the valid contents of the buffer + * have been copied into it. + * + * @return the current contents of this output stream, as a byte array. + */ + public byte[] toByteArray() { + byte newbuf[] = new byte[count]; + System.arraycopy(buf, 0, newbuf, 0, count); + return newbuf; + } + + /** + * Returns the current size of the buffer. + * + * @return the value of the count field, which is the number of valid bytes in this byte buffer. + */ + public int size() { + return count; + } + + public void setSize(int size) { + if (size > count || size < 0) + throw new IndexOutOfBoundsException("The new size must be positive and less or equal of the current size"); + count = size; + } + + /** + * Converts the buffer's contents into a string, translating bytes into + * characters according to the platform's default character encoding. + * + * @return String translated from the buffer's contents. + */ + @Override + public String toString() { + return new String(buf, 0, count); + } + + /** + * Converts the buffer's contents into a string, translating bytes into + * characters according to the specified character encoding. + * + * @param enc a character-encoding name. + * @return String translated from the buffer's contents. + * @throws UnsupportedEncodingException + * If the named encoding is not supported. + */ + public String toString(String enc) throws UnsupportedEncodingException { + return new String(buf, 0, count, enc); + } + + /** + * Writes the complete contents of this byte buffer output to + * the specified output stream argument, as if by calling the output + * stream's write method using out.write(buf, 0, count). + * + * @param out the output stream to which to write the data. + * @exception IOException if an I/O error occurs. + */ + public void writeTo(OutputStream out) throws IOException { + out.write(buf, 0, count); + } + + @Override + public void write(int b) throws IOException { + append((byte)b); + } + + @Override + public void write(byte[] b, int off, int len) { + append(b, off, len); + } + + public byte[] getBuffer() { + return buf; + } + +} + diff --git a/src/main/java/org/pdfparse/utils/DateConverter.java b/src/main/java/org/pdfparse/utils/DateConverter.java new file mode 100755 index 0000000..1760848 --- /dev/null +++ b/src/main/java/org/pdfparse/utils/DateConverter.java @@ -0,0 +1,363 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Modified for pdfparse by Anton Golinko'2013 + +package org.pdfparse.utils; + +import java.text.ParseException; +import java.text.SimpleDateFormat; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.SimpleTimeZone; +import java.util.TimeZone; + +import org.pdfparse.cos.COSString; +import org.pdfparse.exception.*; + +/** + * This class is used to convert dates to strings and back using the PDF + * date standards. Date are described in PDFReference1.4 section 3.8.2 + * + * @author Ben Litchfield + * @version $Revision: 1.14 $ + */ + +// TODO: rewrite parsing method +public class DateConverter +{ + //The Date format is supposed to be the PDF_DATE_FORMAT, but not all PDF documents + //will use that date, so I have added a couple other potential formats + //to try if the original one does not work. + private static final SimpleDateFormat[] POTENTIAL_FORMATS = new SimpleDateFormat[] { + new SimpleDateFormat("EEEE, dd MMM yyyy hh:mm:ss a", Locale.ENGLISH), + new SimpleDateFormat("EEEE, MMM dd, yyyy hh:mm:ss a", Locale.ENGLISH), + new SimpleDateFormat("MM/dd/yyyy hh:mm:ss", Locale.ENGLISH), + new SimpleDateFormat("MM/dd/yyyy", Locale.ENGLISH), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz", Locale.ENGLISH), + new SimpleDateFormat("EEEE, MMM dd, yyyy", Locale.ENGLISH), // Acrobat Distiller 1.0.2 for Macintosh + new SimpleDateFormat("EEEE MMM dd, yyyy HH:mm:ss", Locale.ENGLISH), // ECMP5 + new SimpleDateFormat("EEEE MMM dd HH:mm:ss z yyyy", Locale.ENGLISH), // GNU Ghostscript 7.0.7 + new SimpleDateFormat("EEEE, MMM dd, yyyy 'at' hh:mma", Locale.ENGLISH), // Acrobat Net Distiller 1.0 for Windows + new SimpleDateFormat("d/MM/yyyy hh:mm:ss", Locale.ENGLISH), // PDFBOX-164 + new SimpleDateFormat("dd/MM/yyyy hh:mm:ss", Locale.ENGLISH), // PDFBOX-170 + new SimpleDateFormat("EEEEEEEEEE, MMMMMMMMMMMM dd, yyyy", Locale.ENGLISH), // PDFBOX-465 + new SimpleDateFormat("dd MMM yyyy hh:mm:ss", Locale.ENGLISH), // for 26 May 2000 11:25:00 + new SimpleDateFormat("dd MMM yyyy hh:mm", Locale.ENGLISH), // for 26 May 2000 11:25 + new SimpleDateFormat("M/dd/yyyy hh:mm:ss", Locale.ENGLISH), + new SimpleDateFormat("MM/d/yyyy hh:mm:ss", Locale.ENGLISH), + new SimpleDateFormat("M/dd/yyyy", Locale.ENGLISH), + new SimpleDateFormat("MM/d/yyyy", Locale.ENGLISH), + new SimpleDateFormat("M/d/yyyy hh:mm:ss", Locale.ENGLISH), + new SimpleDateFormat("M/d/yyyy", Locale.ENGLISH), + new SimpleDateFormat("M/d/yy hh:mm:ss", Locale.ENGLISH), + new SimpleDateFormat("M/d/yy", Locale.ENGLISH), + new SimpleDateFormat("yyyymmdd hh:mm:ss Z"), // + new SimpleDateFormat("yyyymmdd hh:mm:ss"), // + new SimpleDateFormat("yyyymmdd'+00''00'''"), // + new SimpleDateFormat("yyyymmdd'+01''00'''"), // + new SimpleDateFormat("yyyymmdd'+02''00'''"), // + new SimpleDateFormat("yyyymmdd'+03''00'''"), // + new SimpleDateFormat("yyyymmdd'+04''00'''"), // + new SimpleDateFormat("yyyymmdd'+05''00'''"), // + new SimpleDateFormat("yyyymmdd'+06''00'''"), // + new SimpleDateFormat("yyyymmdd'+07''00'''"), // + new SimpleDateFormat("yyyymmdd'+08''00'''"), // + new SimpleDateFormat("yyyymmdd'+09''00'''"), // + new SimpleDateFormat("yyyymmdd'+10''00'''"), // + new SimpleDateFormat("yyyymmdd'+11''00'''"), // + new SimpleDateFormat("yyyymmdd'+12''00'''"), // + new SimpleDateFormat("yyyymmdd'-01''00'''"), // + new SimpleDateFormat("yyyymmdd'-02''00'''"), // + new SimpleDateFormat("yyyymmdd'-03''00'''"), // + new SimpleDateFormat("yyyymmdd'-04''00'''"), // + new SimpleDateFormat("yyyymmdd'-05''00'''"), // + new SimpleDateFormat("yyyymmdd'-06''00'''"), // + new SimpleDateFormat("yyyymmdd'-07''00'''"), // + new SimpleDateFormat("yyyymmdd'-08''00'''"), // + new SimpleDateFormat("yyyymmdd'-09''00'''"), // + new SimpleDateFormat("yyyymmdd'-10''00'''"), // + new SimpleDateFormat("yyyymmdd'-11''00'''"), // + new SimpleDateFormat("yyyymmdd'-12''00'''"), // + new SimpleDateFormat("yyyymmdd"), // for 20090401+0200 + }; + + private DateConverter() + { + //utility class should not be constructed. + } + + /** + * This will convert the calendar to a string. + * + * @param date The date to convert to a string. + * + * @return The date as a String to be used in a PDF document. + */ + public static String toString( Calendar date ) + { + String retval = null; + if( date != null ) + { + StringBuffer buffer = new StringBuffer(); + TimeZone zone = date.getTimeZone(); + long offsetInMinutes = zone.getOffset( date.getTimeInMillis() )/1000/60; + long hours = Math.abs( offsetInMinutes/60 ); + long minutes = Math.abs( offsetInMinutes%60 ); + buffer.append( "D:" ); + // PDFBOX-402 , SimpleDateFormat is not thread safe, created it when you use it. + buffer.append( new SimpleDateFormat( "yyyyMMddHHmmss" , Locale.ENGLISH).format( date.getTime() ) ); + if( offsetInMinutes == 0 ) + { + buffer.append( "Z" ); + } + else if( offsetInMinutes < 0 ) + { + buffer.append( "-" ); + } + else + { + buffer.append( "+" ); + } + if( hours < 10 ) + { + buffer.append( "0" ); + } + buffer.append( hours ); + buffer.append( "'" ); + if( minutes < 10 ) + { + buffer.append( "0" ); + } + buffer.append( minutes ); + buffer.append( "'" ); + retval = buffer.toString(); + + } + return retval; + } + + /** + * This will convert a string to a calendar. + * + * @param date The string representation of the calendar. + * + * @return The calendar that this string represents. + * + * @throws EDateConvertError If the date string is not in the correct format. + */ + public static Calendar toCalendar( COSString date ) throws EDateConvertError + { + Calendar retval = null; + if( date != null ) + { + retval = toCalendar( date.getValue() ); + } + + return retval; + } + + /** + * This will convert a string to a calendar. + * + * @param date The string representation of the calendar. + * + * @return The calendar that this string represents. + * + * @throws EDateConvertError If the date string is not in the correct format. + */ + public static Calendar toCalendar( String date ) throws EDateConvertError + { + Calendar retval = null; + if( date != null && date.trim().length() > 0 ) + { + //these are the default values + int year = 0; + int month = 1; + int day = 1; + int hour = 0; + int minute = 0; + int second = 0; + //first string off the prefix if it exists + try + { + SimpleTimeZone zone = null; + if( date.startsWith( "D:" ) ) + { + date = date.substring( 2, date.length() ); + } + if( date.length() < 4 ) + { + throw new EDateConvertError( "Error: Invalid date format '" + date + "'" ); + } + year = Integer.parseInt( date.substring( 0, 4 ) ); + if( date.length() >= 6 ) + { + month = Integer.parseInt( date.substring( 4, 6 ) ); + } + if( date.length() >= 8 ) + { + day = Integer.parseInt( date.substring( 6, 8 ) ); + } + if( date.length() >= 10 ) + { + hour = Integer.parseInt( date.substring( 8, 10 ) ); + } + if( date.length() >= 12 ) + { + minute = Integer.parseInt( date.substring( 10, 12 ) ); + } + if( date.length() >= 14 ) + { + second = Integer.parseInt( date.substring( 12, 14 ) ); + } + + if( date.length() >= 15 ) + { + char sign = date.charAt( 14 ); + if( sign == 'Z' ) + { + zone = new SimpleTimeZone(0,"Unknown"); + } + else + { + int hours = 0; + int minutes = 0; + if( date.length() >= 17 ) + { + if( sign == '+' ) + { + //parseInt cannot handle the + sign + hours = Integer.parseInt( date.substring( 15, 17 ) ); + } + else if (sign == '-') + { + hours = -Integer.parseInt(date.substring(15,17)); + } + else + { + hours = -Integer.parseInt( date.substring( 14, 16 ) ); + } + } + if( date.length() > 20 ) + { + minutes = Integer.parseInt( date.substring( 18, 20 ) ); + } + zone = new SimpleTimeZone( hours*60*60*1000 + minutes*60*1000, "Unknown" ); + } + } + if( zone != null ) + { + retval = new GregorianCalendar( zone ); + } + else + { + retval = new GregorianCalendar(); + } + + retval.set(year, month-1, day, hour, minute, second ); + // PDF dates are only accurate up to a second + retval.set(Calendar.MILLISECOND, 0); + } + catch( NumberFormatException e ) + { + for( int i=0; retval == null && iA hash map that uses primitive ints for the key rather than objects.

+ * + *

Note that this class is for internal optimization purposes only, and may + * not be supported in future releases of Jakarta Commons Lang. Utilities of + * this sort may be included in future releases of Jakarta Commons Collections.

+ * + * @author Justin Couch + * @author Alex Chaffee (alex@apache.org) + * @author Stephen Colebourne + * @author Bruno Lowagie (change Objects as keys into int values) + * @author Paulo Soares (added extra methods) + */ +public class IntIntHashtable implements Cloneable { + + /*** + * The hash table data. + */ + private transient Entry table[]; + + /*** + * The total number of entries in the hash table. + */ + private transient int count; + + /*** + * The table is rehashed when its size exceeds this threshold. (The + * value of this field is (int)(capacity * loadFactor).) + * + * @serial + */ + private int threshold; + + /*** + * The load factor for the hashtable. + * + * @serial + */ + private float loadFactor; + + /*** + *

Constructs a new, empty hashtable with a default capacity and load + * factor, which is 20 and 0.75 respectively.

+ */ + public IntIntHashtable() { + this(150, 0.75f); + } + + /*** + *

Constructs a new, empty hashtable with the specified initial capacity + * and default load factor, which is 0.75.

+ * + * @param initialCapacity the initial capacity of the hashtable. + * @throws IllegalArgumentException if the initial capacity is less + * than zero. + */ + public IntIntHashtable(int initialCapacity) { + this(initialCapacity, 0.75f); + } + + /*** + *

Constructs a new, empty hashtable with the specified initial + * capacity and the specified load factor.

+ * + * @param initialCapacity the initial capacity of the hashtable. + * @param loadFactor the load factor of the hashtable. + * @throws IllegalArgumentException if the initial capacity is less + * than zero, or if the load factor is nonpositive. + */ + public IntIntHashtable(int initialCapacity, float loadFactor) { + super(); + if (initialCapacity < 0) { + throw new IllegalArgumentException(String.format("Illegal capacity %d", initialCapacity)); + } + if (loadFactor <= 0) { + throw new IllegalArgumentException(String.format("Illegal load %s", String.valueOf(loadFactor))); + } + if (initialCapacity == 0) { + initialCapacity = 1; + } + this.loadFactor = loadFactor; + table = new Entry[initialCapacity]; + threshold = (int) (initialCapacity * loadFactor); + } + + /*** + *

Returns the number of keys in this hashtable.

+ * + * @return the number of keys in this hashtable. + */ + public int size() { + return count; + } + + /*** + *

Tests if this hashtable maps no keys to values.

+ * + * @return true if this hashtable maps no keys to values; + * false otherwise. + */ + public boolean isEmpty() { + return count == 0; + } + + /*** + *

Tests if some key maps into the specified value in this hashtable. + * This operation is more expensive than the containsKey + * method.

+ * + *

Note that this method is identical in functionality to containsValue, + * (which is part of the Map interface in the collections framework).

+ * + * @param value a value to search for. + * @return true if and only if some key maps to the + * value argument in this hashtable as + * determined by the equals method; + * false otherwise. + * @throws NullPointerException if the value is null. + * @see #containsKey(int) + * @see #containsValue(int) + * @see java.util.Map + */ + public boolean contains(int value) { + + Entry tab[] = table; + for (int i = tab.length; i-- > 0;) { + for (Entry e = tab[i]; e != null; e = e.next) { + if (e.value == value) { + return true; + } + } + } + return false; + } + + /*** + *

Returns true if this HashMap maps one or more keys + * to this value.

+ * + *

Note that this method is identical in functionality to contains + * (which predates the Map interface).

+ * + * @param value value whose presence in this HashMap is to be tested. + * @return boolean true if the value is contained + * @see java.util.Map + * @since JDK1.2 + */ + public boolean containsValue(int value) { + return contains(value); + } + + /*** + *

Tests if the specified int is a key in this hashtable.

+ * + * @param key possible key. + * @return true if and only if the specified int is a + * key in this hashtable, as determined by the equals + * method; false otherwise. + * @see #contains(int) + */ + public boolean containsKey(int key) { + Entry tab[] = table; + int hash = key; + int index = (hash & 0x7FFFFFFF) % tab.length; + for (Entry e = tab[index]; e != null; e = e.next) { + if (e.hash == hash && e.key == key) { + return true; + } + } + return false; + } + + /*** + *

Returns the value to which the specified key is mapped in this map.

+ * + * @param key a key in the hashtable. + * @return the value to which the key is mapped in this hashtable; + * null if the key is not mapped to any value in + * this hashtable. + * @see #put(int, int) + */ + public int get(int key) { + Entry tab[] = table; + int hash = key; + int index = (hash & 0x7FFFFFFF) % tab.length; + for (Entry e = tab[index]; e != null; e = e.next) { + if (e.hash == hash && e.key == key) { + return e.value; + } + } + return 0; + } + + /*** + *

Increases the capacity of and internally reorganizes this + * hashtable, in order to accommodate and access its entries more + * efficiently.

+ * + *

This method is called automatically when the number of keys + * in the hashtable exceeds this hashtable's capacity and load + * factor.

+ */ + protected void rehash() { + int oldCapacity = table.length; + Entry oldMap[] = table; + + int newCapacity = oldCapacity * 2 + 1; + Entry newMap[] = new Entry[newCapacity]; + + threshold = (int) (newCapacity * loadFactor); + table = newMap; + + for (int i = oldCapacity; i-- > 0;) { + for (Entry old = oldMap[i]; old != null;) { + Entry e = old; + old = old.next; + + int index = (e.hash & 0x7FFFFFFF) % newCapacity; + e.next = newMap[index]; + newMap[index] = e; + } + } + } + + /*** + *

Maps the specified key to the specified + * value in this hashtable. The key cannot be + * null.

+ * + *

The value can be retrieved by calling the get method + * with a key that is equal to the original key.

+ * + * @param key the hashtable key. + * @param value the value. + * @return the previous value of the specified key in this hashtable, + * or null if it did not have one. + * @throws NullPointerException if the key is null. + * @see #get(int) + */ + public int put(int key, int value) { + // Makes sure the key is not already in the hashtable. + Entry tab[] = table; + int hash = key; + int index = (hash & 0x7FFFFFFF) % tab.length; + for (Entry e = tab[index]; e != null; e = e.next) { + if (e.hash == hash && e.key == key) { + int old = e.value; + e.value = value; + return old; + } + } + + if (count >= threshold) { + // Rehash the table if the threshold is exceeded + rehash(); + + tab = table; + index = (hash & 0x7FFFFFFF) % tab.length; + } + + // Creates the new entry. + Entry e = new Entry(hash, key, value, tab[index]); + tab[index] = e; + count++; + return 0; + } + + /*** + *

Removes the key (and its corresponding value) from this + * hashtable.

+ * + *

This method does nothing if the key is not present in the + * hashtable.

+ * + * @param key the key that needs to be removed. + * @return the value to which the key had been mapped in this hashtable, + * or null if the key did not have a mapping. + */ + public int remove(int key) { + Entry tab[] = table; + int hash = key; + int index = (hash & 0x7FFFFFFF) % tab.length; + for (Entry e = tab[index], prev = null; e != null; prev = e, e = e.next) { + if (e.hash == hash && e.key == key) { + if (prev != null) { + prev.next = e.next; + } else { + tab[index] = e.next; + } + count--; + int oldValue = e.value; + e.value = 0; + return oldValue; + } + } + return 0; + } + + /*** + *

Clears this hashtable so that it contains no keys.

+ */ + public void clear() { + Entry tab[] = table; + for (int index = tab.length; --index >= 0;) { + tab[index] = null; + } + count = 0; + } + + /*** + *

Innerclass that acts as a datastructure to create a new entry in the + * table.

+ */ + static class Entry { + int hash; + int key; + int value; + Entry next; + + /*** + *

Create a new entry with the given values.

+ * + * @param hash The code used to hash the int with + * @param key The key used to enter this in the table + * @param value The value for this key + * @param next A reference to the next entry in the table + */ + protected Entry(int hash, int key, int value, Entry next) { + this.hash = hash; + this.key = key; + this.value = value; + this.next = next; + } + + // extra methods for inner class Entry by Paulo + public int getKey() { + return key; + } + public int getValue() { + return value; + } + @Override + protected Object clone() { + Entry entry = new Entry(hash, key, value, next != null ? (Entry)next.clone() : null); + return entry; + } + } + + // extra inner class by Paulo + static class IntHashtableIterator implements Iterator { + int index; + Entry table[]; + Entry entry; + + IntHashtableIterator(Entry table[]) { + this.table = table; + this.index = table.length; + } + public boolean hasNext() { + if (entry != null) { + return true; + } + while (index-- > 0) { + if ((entry = table[index]) != null) { + return true; + } + } + return false; + } + + @Override + public Entry next() { + if (entry == null) { + while (index-- > 0 && (entry = table[index]) == null); + } + if (entry != null) { + Entry e = entry; + entry = e.next; + return e; + } + throw new NoSuchElementException("inthashtableiterator"); + } + @Override + public void remove() { + throw new UnsupportedOperationException("Remove not supported"); + } + } + +// extra methods by Paulo Soares: + + public Iterator getEntryIterator() { + return new IntHashtableIterator(table); + } + + public int[] toOrderedKeys() { + int res[] = getKeys(); + Arrays.sort(res); + return res; + } + + public int[] getKeys() { + int res[] = new int[count]; + int ptr = 0; + int index = table.length; + Entry entry = null; + while (true) { + if (entry == null) + while (index-- > 0 && (entry = table[index]) == null); + if (entry == null) + break; + Entry e = entry; + entry = e.next; + res[ptr++] = e.key; + } + return res; + } + + public int getOneKey() { + if (count == 0) + return 0; + int index = table.length; + Entry entry = null; + while (index-- > 0 && (entry = table[index]) == null); + if (entry == null) + return 0; + return entry.key; + } + + @Override + public Object clone() { + try { + IntIntHashtable t = (IntIntHashtable)super.clone(); + t.table = new Entry[table.length]; + for (int i = table.length ; i-- > 0 ; ) { + t.table[i] = table[i] != null + ? (Entry)table[i].clone() : null; + } + return t; + } catch (CloneNotSupportedException e) { + // this shouldn't happen, since we are Cloneable + throw new InternalError(); + } + } +} diff --git a/src/main/java/org/pdfparse/utils/IntObjHashtable.java b/src/main/java/org/pdfparse/utils/IntObjHashtable.java new file mode 100755 index 0000000..c1e4a89 --- /dev/null +++ b/src/main/java/org/pdfparse/utils/IntObjHashtable.java @@ -0,0 +1,480 @@ +/* + * This class is based on org.apache.IntHashMap.commons.lang + * http://jakarta.apache.org/commons/lang/xref/org/apache/commons/lang/IntHashMap.html + * It was adapted by Bruno Lowagie for use in iText, + * reusing methods that were written by Paulo Soares. + * Instead of being a hashtable that stores objects with an int as key, + * it stores int values with an int as key. + * + * This is the original license of the original class IntHashMap: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Note: originally released under the GNU LGPL v2.1, + * but rereleased by the original author under the ASF license (above). + */ + +package org.pdfparse.utils; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; + + +/*** + *

A hash map that uses primitive ints for the key rather than objects.

+ * + *

Note that this class is for internal optimization purposes only, and may + * not be supported in future releases of Jakarta Commons Lang. Utilities of + * this sort may be included in future releases of Jakarta Commons Collections.

+ * + * @author Justin Couch + * @author Alex Chaffee (alex@apache.org) + * @author Stephen Colebourne + * @author Bruno Lowagie (change Objects as keys into int values) + * @author Paulo Soares (added extra methods) + */ +public class IntObjHashtable implements Cloneable { + + /*** + * The hash table data. + */ + private transient Entry table[]; + + /*** + * The total number of entries in the hash table. + */ + private transient int count; + + /*** + * The table is rehashed when its size exceeds this threshold. (The + * value of this field is (int)(capacity * loadFactor).) + * + * @serial + */ + private int threshold; + + /*** + * The load factor for the hashtable. + * + * @serial + */ + private float loadFactor; + + /*** + *

Constructs a new, empty hashtable with a default capacity and load + * factor, which is 20 and 0.75 respectively.

+ */ + public IntObjHashtable() { + this(150, 0.75f); + } + + /*** + *

Constructs a new, empty hashtable with the specified initial capacity + * and default load factor, which is 0.75.

+ * + * @param initialCapacity the initial capacity of the hashtable. + * @throws IllegalArgumentException if the initial capacity is less + * than zero. + */ + public IntObjHashtable(int initialCapacity) { + this(initialCapacity, 0.75f); + } + + /*** + *

Constructs a new, empty hashtable with the specified initial + * capacity and the specified load factor.

+ * + * @param initialCapacity the initial capacity of the hashtable. + * @param loadFactor the load factor of the hashtable. + * @throws IllegalArgumentException if the initial capacity is less + * than zero, or if the load factor is nonpositive. + */ + public IntObjHashtable(int initialCapacity, float loadFactor) { + super(); + if (initialCapacity < 0) { + throw new IllegalArgumentException(String.format("Illegal capacity %d", initialCapacity)); + } + if (loadFactor <= 0) { + throw new IllegalArgumentException(String.format("Illegal load %s", String.valueOf(loadFactor))); + } + if (initialCapacity == 0) { + initialCapacity = 1; + } + this.loadFactor = loadFactor; + table = new Entry[initialCapacity]; + threshold = (int) (initialCapacity * loadFactor); + } + + /*** + *

Returns the number of keys in this hashtable.

+ * + * @return the number of keys in this hashtable. + */ + public int size() { + return count; + } + + /*** + *

Tests if this hashtable maps no keys to values.

+ * + * @return true if this hashtable maps no keys to values; + * false otherwise. + */ + public boolean isEmpty() { + return count == 0; + } + + /*** + *

Tests if some key maps into the specified value in this hashtable. + * This operation is more expensive than the containsKey + * method.

+ * + *

Note that this method is identical in functionality to containsValue, + * (which is part of the Map interface in the collections framework).

+ * + * @param value a value to search for. + * @return true if and only if some key maps to the + * value argument in this hashtable as + * determined by the equals method; + * false otherwise. + * @throws NullPointerException if the value is null. + * @see #containsKey(int) + * @see #containsValue(V) + * @see java.util.Map + */ + public boolean contains(V value) { + + Entry tab[] = table; + for (int i = tab.length; i-- > 0;) { + for (Entry e = tab[i]; e != null; e = e.next) { + if (e.value == value) { + return true; + } + } + } + return false; + } + + /*** + *

Returns true if this HashMap maps one or more keys + * to this value.

+ * + *

Note that this method is identical in functionality to contains + * (which predates the Map interface).

+ * + * @param value value whose presence in this HashMap is to be tested. + * @return boolean true if the value is contained + * @see java.util.Map + * @since JDK1.2 + */ + public boolean containsValue(V value) { + return contains(value); + } + + /*** + *

Tests if the specified int is a key in this hashtable.

+ * + * @param key possible key. + * @return true if and only if the specified int is a + * key in this hashtable, as determined by the equals + * method; false otherwise. + * @see #contains(V) + */ + public boolean containsKey(int key) { + Entry tab[] = table; + int hash = key; + int index = (hash & 0x7FFFFFFF) % tab.length; + for (Entry e = tab[index]; e != null; e = e.next) { + if (e.hash == hash && e.key == key) { + return true; + } + } + return false; + } + + /*** + *

Returns the value to which the specified key is mapped in this map.

+ * + * @param key a key in the hashtable. + * @return the value to which the key is mapped in this hashtable; + * null if the key is not mapped to any value in + * this hashtable. + * @see #put(int, V) + */ + public V get(int key) { + Entry tab[] = table; + int hash = key; + int index = (hash & 0x7FFFFFFF) % tab.length; + for (Entry e = tab[index]; e != null; e = e.next) { + if (e.hash == hash && e.key == key) { + return e.value; + } + } + return null; + } + + /*** + *

Increases the capacity of and internally reorganizes this + * hashtable, in order to accommodate and access its entries more + * efficiently.

+ * + *

This method is called automatically when the number of keys + * in the hashtable exceeds this hashtable's capacity and load + * factor.

+ */ + protected void rehash() { + int oldCapacity = table.length; + Entry oldMap[] = table; + + int newCapacity = oldCapacity * 2 + 1; + Entry newMap[] = new Entry[newCapacity]; + + threshold = (int) (newCapacity * loadFactor); + table = newMap; + + for (int i = oldCapacity; i-- > 0;) { + for (Entry old = oldMap[i]; old != null;) { + Entry e = old; + old = old.next; + + int index = (e.hash & 0x7FFFFFFF) % newCapacity; + e.next = newMap[index]; + newMap[index] = e; + } + } + } + + /*** + *

Maps the specified key to the specified + * value in this hashtable. The key cannot be + * null.

+ * + *

The value can be retrieved by calling the get method + * with a key that is equal to the original key.

+ * + * @param key the hashtable key. + * @param value the value. + * @return the previous value of the specified key in this hashtable, + * or null if it did not have one. + * @throws NullPointerException if the key is null. + * @see #get(int) + */ + public V put(int key, V value) { + // Makes sure the key is not already in the hashtable. + Entry tab[] = table; + int hash = key; + int index = (hash & 0x7FFFFFFF) % tab.length; + for (Entry e = tab[index]; e != null; e = e.next) { + if (e.hash == hash && e.key == key) { + V old = e.value; + e.value = value; + return old; + } + } + + if (count >= threshold) { + // Rehash the table if the threshold is exceeded + rehash(); + + tab = table; + index = (hash & 0x7FFFFFFF) % tab.length; + } + + // Creates the new entry. + Entry e = new Entry(hash, key, value, tab[index]); + tab[index] = e; + count++; + return null; + } + + /*** + *

Removes the key (and its corresponding value) from this + * hashtable.

+ * + *

This method does nothing if the key is not present in the + * hashtable.

+ * + * @param key the key that needs to be removed. + * @return the value to which the key had been mapped in this hashtable, + * or null if the key did not have a mapping. + */ + public V remove(int key) { + Entry tab[] = table; + int hash = key; + int index = (hash & 0x7FFFFFFF) % tab.length; + for (Entry e = tab[index], prev = null; e != null; prev = e, e = e.next) { + if (e.hash == hash && e.key == key) { + if (prev != null) { + prev.next = e.next; + } else { + tab[index] = e.next; + } + count--; + V oldValue = e.value; + e.value = null; + return oldValue; + } + } + return null; + } + + /*** + *

Clears this hashtable so that it contains no keys.

+ */ + public void clear() { + Entry tab[] = table; + for (int index = tab.length; --index >= 0;) { + tab[index] = null; + } + count = 0; + } + + /*** + *

Innerclass that acts as a datastructure to create a new entry in the + * table.

+ */ + static class Entry { + int hash; + int key; + V value; + Entry next; + + /*** + *

Create a new entry with the given values.

+ * + * @param hash The code used to hash the int with + * @param key The key used to enter this in the table + * @param value The value for this key + * @param next A reference to the next entry in the table + */ + protected Entry(int hash, int key, V value, Entry next) { + this.hash = hash; + this.key = key; + this.value = value; + this.next = next; + } + + // extra methods for inner class Entry by Paulo + public int getKey() { + return key; + } + public V getValue() { + return value; + } + @Override + protected Object clone() { + Entry entry = new Entry(hash, key, value, next != null ? (Entry)next.clone() : null); + return entry; + } + } + + // extra inner class by Paulo + static class IntHashtableIterator implements Iterator> { + int index; + Entry table[]; + Entry entry; + + IntHashtableIterator(Entry table[]) { + this.table = table; + this.index = table.length; + } + public boolean hasNext() { + if (entry != null) { + return true; + } + while (index-- > 0) { + if ((entry = table[index]) != null) { + return true; + } + } + return false; + } + + @Override + public Entry next() { + if (entry == null) { + while (index-- > 0 && (entry = table[index]) == null); + } + if (entry != null) { + Entry e = entry; + entry = e.next; + return e; + } + throw new NoSuchElementException("inthashtableiterator"); + } + @Override + public void remove() { + throw new UnsupportedOperationException("Remove not supported"); + } + } + +// extra methods by Paulo Soares: + + public Iterator> getEntryIterator() { + return new IntHashtableIterator(table); + } + + public int[] toOrderedKeys() { + int res[] = getKeys(); + Arrays.sort(res); + return res; + } + + public int[] getKeys() { + int res[] = new int[count]; + int ptr = 0; + int index = table.length; + Entry entry = null; + while (true) { + if (entry == null) + while (index-- > 0 && (entry = table[index]) == null); + if (entry == null) + break; + Entry e = entry; + entry = e.next; + res[ptr++] = e.key; + } + return res; + } + + public int getOneKey() { + if (count == 0) + return 0; + int index = table.length; + Entry entry = null; + while (index-- > 0 && (entry = table[index]) == null); + if (entry == null) + return 0; + return entry.key; + } + + @Override + public Object clone() { + try { + IntObjHashtable t = (IntObjHashtable)super.clone(); + t.table = new Entry[table.length]; + for (int i = table.length ; i-- > 0 ; ) { + t.table[i] = table[i] != null + ? (Entry)table[i].clone() : null; + } + return t; + } catch (CloneNotSupportedException e) { + // this shouldn't happen, since we are Cloneable + throw new InternalError(); + } + } +} diff --git a/src/main/res/drawable-hdpi/ic_action_content_new.png b/src/main/res/drawable-hdpi/ic_action_content_new.png new file mode 100755 index 0000000000000000000000000000000000000000..f002f1946dadaa49507cbf3873550dbab011743f GIT binary patch literal 322 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw1|+Ti+$>^XVC3|4aSX|Demf(P>yQCY>+Q)e z&t92*cJfuWKZeVaF7BDQG=AZRgC{0^-&>Nn)o0tQl&@l3=lo<23HU8znC8d!WvL8@ zv&BrN2rJ0|K3$#OrIR%qmR@y;&VK%n@AkH7QLh)(J-7PzuBH0m^?3)L&OEYKJm>pv zU9K>1-b9_XUnOkUT-`2G_2Y!?9!V{`nNPRK+5GzYw|jL=yYO|ndpEAGwg$4e{;{7f znINs4s{fkG?sdSBq ym3b{U1o|8tXEL`;edX4{`;bFqilqZSVg8=2MYe1Ir`-dF4TGnvpUXO@geCwpqL6+7 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_browse.png b/src/main/res/drawable-hdpi/ic_browse.png new file mode 100755 index 0000000000000000000000000000000000000000..dd71ad502e908765a78b572c8ef55a69d4b0f074 GIT binary patch literal 1139 zcmV-(1dRKMP)sR%{r!CFM{ft0r5LA+RBC@NybYKz5QDk8qU2trj_ z5UU5N3dUm}DMigiVzdYK5UNQkw9-8MMrPx7Y~o~R!>-9L4B2Go|L6Pu@1K8XCZ?hS zyG#Yfx5yxz?}{4=8w%%H1dNoDWoM#a*QaaBBHHucwQ`+Tn&2u;$^~q$i z9^rPQtwx(qL;oG0AJBXzlW8Cv4i5wZfuFg|vYZjFtgLicES3`he*kSECy5F4yBUwi zFO7|jg*k1q>XsA29*?IPie3Sd@~qJO=^TRMM@L3R9w@X=Yo;pU+S*!cG#b4Iu=^DN zjlsq$w?oiwmzI_u_IkbKnJSlRgbCTn$;n=z+NlH?g46+m?_t}8wnFlO-yhj*wnnu` zt46r0s_K?PWq)&n?GB`He@#t|eQIjz4!$=@q>0j_qoXZK0?FE_LYU6k`vQO-`+UA# zuuo7QCb!$&jr-#=rFW|_r-cdAYD`j*w?KKJ-fKbx%EfX221K69$fh`gbPv8X(;q^p z-fKjww=iLVx&T0DdDSIz6$IOP`jhmYo}PY-zAI_}PGmwGk8Qd>LWIepqO7d!Go16j zXUSaUCQLwMx-=mZq}S6#BobLBvN#bU3^3aOra#Mww?U;&UYmx721_s)450riTHLlH zL!R*3t6H>>&e!4aU>1XqWA(LpXlRJcdF~72Ttw$qYH$MqJH&0wbQDq;KnCG&J@E;p zpCMhhus{G#Xbj@7y1IH(B9VAOeGkIt2CWE(Fz_NWxYcT19*f2H;kH8#Mh(IU>=O9= zLj8u|bFo&0$&$}P2QLAT8Gt({C#?oyx{8~jzSaZU5yS-Ri{;{Mvz-64<4Bme|0E| z&-GBW;foh1CZhlz<2RwZ>G7Hjh!fk?YZJEmH~^3lVvl%wkd7^Edh^U@HW<1);voRj z9O8{bnO3LM+1uCG7pK|q&jKbVtlL98iFBG{+HpL82&G7t~)Za^lRNX&yXtqAj3(zTq>7_W%Ex#xqB?=p(c})B>>RmfPQ@Snmpn;ww0Xe*y6k;!G8|;`9Ij002ovPDHLk FV1lcp7lQx* literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_collection_black.png b/src/main/res/drawable-hdpi/ic_collection_black.png new file mode 100755 index 0000000000000000000000000000000000000000..70b9f507c7bfae68b68859d8b1d23892198bdd56 GIT binary patch literal 554 zcmV+_0@eMAP)C1p8z`t?tj2;0dINJa58w$D!2@^$4FnTE)i4XQv*YyZ z>h9Oe%o}*fy04~QRlV6@dORA~H6EDWkqPd5;+C)_+%pGkrF$0Wj2uhY67HD;w$eQd z)PA&bukTAvC%K_-W8H=rif-@8S~c zk6mdKT(jDjSThx0|J)}mUNd2%2^JbP)t2)aX6EU_Si+2>mT;hJN!CornS?84!osq(RdZ$i&a15L0nnnZ1MP&BY5VfPv*=QVj<@92o6E(3GYZEmQXf5pRd;p(7V`2;V29g7@fptz| zA_&~h?Ax|%ggN1aIlv>G zut52t73)an8~!ddxB;8D;F}4Vo*`U+RYDxMOWspllNOucLao0cGJ`iE)GCg=?1^D^ zvsGDs$Kmv^3a z!-UPc&Vs4VP2#YLVe6?pjS|k^sU`W@y@ZXT10b+QC83Gy2qo^-O75(ED8Do@(f$q> z>i#W=tx%hja7xr#W$%cD4W1`dV1O$${0|GsPEzc&O5Qq^x2a%wPZ)iG#MoNcdorvZ z%$?Z_Bc26g+?wZlSYp_x`obrSF;1tJ1?#vpr6#s}n8;@)j1wk0$_ZnPi)zetdPdbN zvV?U=M^!WVl7m^71!LU$J7yT1X)Py=a4@ow4z_U+ebJ@A+eOp^XVC?dAaSX|Demlc3?~sFlYvgi8 zp=m`;cNlN$PKpk4-RaTCa7RIXVb7FvK~cBwGzzK=;<{g`bb{wAOQrND0@FAaZ!(rKH4IS2J90dPQlTcv$`Taxt z0oT3{R=WaGplNqX=j9f=MdIRrCM&-Bp{$Tz~oZ+vU5~+3TMF zU+Ht?!$Z^a%DjJtTrVEhbuN$n?z-VxN_q9^o!Rq6xoh4s+&Ztg!L0CYoxvL4nBDR` r*}~V~u^+y5Wb5yP=)&T29@@U}nqnKh;$<5!au_^a{an^LB{Ts59{bNZ literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_down.png b/src/main/res/drawable-hdpi/ic_down.png new file mode 100755 index 0000000000000000000000000000000000000000..490d93a9449a67e7ddc8b09d80c5ccad2c3be0bc GIT binary patch literal 467 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw1|+Ti+$>^XV4UUY;uw#oT8pzl~(KxHOORY4j}RvlIVSv-`|@ zzT7pi=1s8EwK*qe`A1%UyE*S=exP#hCY|W7we>ek@A}4GfA7tt>RFW6Q^|Gs!`4-+ ztXWK0A`;|P*6_-3d^s)jg7eMQ4etu~#?@c9c*4r)clk@0&RUffkxaJjThmhi)<#a- zcFa+qV}q|n`?ToL+uN@DUf$!&`N(2l$D$XJ?@|xfN~~GS&m70}R!WR-5AQ9z;>@x+ zS}PuY^skeC|8VAH!+Cj%>IbBK`Yjh!m;F0d%(jQ|mgbttwl?oSr0ik7a&hxpnc}R9 zUCW<#zhWso_s(j|f7Xg{jVHxS%DY!DG~>D|`=2=_nXqFxa$PD(mtF uHtrKVCOvP~wcRpVbCAi1nHRhA8pbcmt+t!2Vk3c(#o+1c=d#Wzp$PyvJkrJh literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_edit.png b/src/main/res/drawable-hdpi/ic_edit.png new file mode 100755 index 0000000000000000000000000000000000000000..c8306b2653e39f75e72a0790d2a378c6ac112293 GIT binary patch literal 556 zcmV+{0@MA8P)Kma=veFKZr2QUF=lf#_I z5p-vVfg2JM3U+6|`Tv>Ct~FY*$67JokpK@}aZy+l9#{s%(gPFp7mg?_3J)v;V(Ebi zDosl)&B4;X@9!2xapE}6G>+p}%r&u7*z>%7%d+kPavk0oe2UBgSdu@bHh`W$m>BDZt{Z0Xb=&|w&E zMN#yoW8=lKq`)m;zoDU9pO%db3s%iT2K)zDwJx-pttN{Em^Ee+~Upl+`?+Zgrm&=U9^j(EqIw~s9nT>A@!!XMy(F`RtT|1J`9oM~WIuqaKd z!P4wtv4D?YBK20EI}SD5$$1#F26gAsu3_B;P>&a2>Wovz6NRZWP90Aarp`EZJW-fB u+Ue#t_a-pjK>Ve3^!~9 z;*@S!Lfdd+hGT{sHUe=}R1LbwSsi;Z=ZLNx-e<{AyEyZvnO>0jFzAn+QA) zs0SHf^SS=84(K|k^l5-LXF#3d`j#dEPXXpR8k+(2mFvCeoi%jX49_LNsYvfuL1~|M zJi!y>lY`c~S<(O`kKb_Zv1fa2W@Uj_(&9xlu4n)`{k~wo4qeOX_t(5Euo_x~uDeVZ zjl;)JnO%YI`WR=~Q~|8EXQCTuPMKha@O{F)<>*D(_NxL`921c~AeNO4D&c>VbA7OU ztfJj2ffdhaq}NY>6~WQQAkypVdqwS;{h79p01u#|1Ns}^{pes-psjX_2ILdRdW_DZ zF+Rrs*?i-86S|Po&&%8rcmxsXJv!Mdl7B|;In+D-S57}Kb4y@-lsxi^qTNWodNNnO zSpsh*+LHw%-iF_)g1$q#SputWwV`0p=kYVCcUb~IC1_PM(ze$6)H+YwR27+BaIk`8 zcpAN`e3Q1_niF^O4zUE z{PJ)CK1!fFU9kFy7(}~KR!LSrDUYCj)JnoJj&}_gU`^WvNF^Oj-`k#n_hF}Lu9DAv zlr`Nwn`<|-n9}3vMh~3=VOe24>g5HlP&0p{Ju44V&e e;0`)2!+!vPv0XhY_wGdi0000XG!RUEblb4?Vf&TCF0!L=KY7)I~coo&+q(xpWpX)&iQ$OY_bn* zGG1Z7O4?+CTAP+3B`t)09Av2%J=@>tT~WpW z2j?CnfW1X$O%GfZl3X$+pBX3zWY?I7kLU@lyTU?1x`O|O-%LPK(k}Y=Tc!oB^Hcjo zNmNA8@&qK1$%<)*@X8M{y`Xhf7}c#u$fv8|C^>;D5111;Bn_gvhm+t2MK51xI$$9f z-G_^II5im5h+fBgJ)m_fX(1%_pt|Q%LIJridiW;O0PpUUoN+q|Bha=qPz$ygagVm~ zGg?C{QW($FYD%FLgJfr`6M(wdGj2>yA`=${;z9>b{993$Aaj(E8K5LWIQ&w=5)eTj z5j?XK&*)pT)6kMf^xAJIN4q$!KAarG9LliepGDTI-zua zRe)I#?mM$Ppuj$W>+XospjO!fc}W*z_2Em!-_@8`HDDG@Z8y!EmFIgXrRP)$4xq1? zD{LBShTm$;$L3HKcsVFMbFZS(*A&c8F|rOkn{TKEx;&32z=?GojGo5byB8nxa}0V3 zb!f!S>rsw&Sq;|=nD{9txT5Ur+?iD*D}%m?5r>obLC(=GtKpgfv!(=_kq=o#ku=CA zlj_k{eq=qinQSxzc88>9?3lVT1(51Jc4#eO58u5W+e|i^0b>`2_f~KwQvj*o(}!B! zcWDS59mh>eNCUjQKDpHILBvg~a(&7Azm+Dy<)MYLJQ|-&<9Y~NQD5Rr@2S7_Hn19n>0W?@vfiRDrch2xMta#-97CH=6VIo zu5!aAQ7@K%NB)*Y_oqguqy#Y8R&`)@ofETxE$nklDGAgah?d&pG_LlwkLtimAoZ}d zfc40-kv=Sijm*9tzAc6I$g+_>EQO8Cz8=0Uh4sj?kv=Sijm*9tzAc6I$g+_>Hl4zM Y09>LSOyIWf@c;k-07*qoM6N<$f@|yU3IG5A literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_isbn.png b/src/main/res/drawable-hdpi/ic_isbn.png new file mode 100755 index 0000000000000000000000000000000000000000..88ca7a12c138f0d294c731e6e80d061b8e4779ed GIT binary patch literal 1032 zcmV+j1o!)iP)YmqjlH_r*q z^^Cg=yA0>p47ifcv4pme<1*|roMSWKN;=09>J}|m(ypX)YzACO=U77BD&6Xmjz*(h znZ{{g4wxH>M7}4g68y&Uel?f~`hd^iAvj3P>G&A@#Z7%yfzhBlcn|J^eZ(|oQjHl2 zHe?DJa6Xs?dYQrVA@Vf=`Bl8z3EC$ixu3Wdo{j!HaDM{KPU0WseJwGY(-obzV93-k z$U-nU!C)-}<6nZ`MbOUpzu@`^j063wKFSerLC?gjpM)hU{Ce zLm~eqp1di0t>F4Bx*7W~u50nv_nlS?CBulA3QMg4+NQMHRzUu2BIfw+;(c|T?H13A zh|$WOR?E6z1WbTo1IRZ_cR*a*P{M|P1qQA1S^&obZ}tAjeWACW_*I=%!H^#gF@1Br z4Z(V@^C11(4`Jd}5hsBTJ8I{Mo#`jm^U0cE$o6N@>%co~D9hTDf22d0ekPj=QP>CK zbxif$vLYCgePPsOm7~tXrI2loXNvCt-sxEBwKCfeyUJUK8MS6?I>7jDfeSV#s>?9a zQBqB(sA7n|OT^Ngru38Go#txr-6CJJm7U^thKaq8&Fj~f=xhi+4wD5I8!a|1wjX1! zM8Iu?9pHJX74v}mS6~bnWWBG#|A;T8#wiA27(^nWjN>=4!eJs;jl9h^!P5rLCZI!+!x@i$+W)GA|wRS@(9-XJ$nS6;@2Sq38d274n;hN;f0?oQ7O z6oE|jtNQA7x&s-H2KE{cjCUm9+8vj}a=5YyXiHaS=sz4eEQc$bfVOmHhU(B_Zs{$I z^A2%_*fZ{wTjXCE*F>c=fgjMpMN}8Ri;_Syfj`jnff4ux`G<&|u+ANt3A{kln?~Rp z$z6R_`D(F&xn~jkKx+LHv(4_&pLzf8b3SBC28#JX?ib%MMVy>lEtbOaa z%8u~tI6SBHN)cHB*m7-CdZma6*y3CNT#7jq5jp(VHS18nmzZwVX_cXf1m!QhZ?sgg z)>s>0R--H(cZZiX+W%PU&b00n0XGw%Of8I34oChZT>I~^br%UZa;NCf(xJ8Dg*muW z`V#}Q{>9RF#xo@D_f77~v!0+`T2FX*&S@(sL zF=uEpff@a{YY5nV#F-1o;miiOmzTrtBhFkv4reyNy}TTDA93aaayYXA?&amM`-n3a Z_yIv%Lp|*JjK2T?002ovPDHLkV1j@&-6{Y8 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_item_blue.png b/src/main/res/drawable-hdpi/ic_item_blue.png new file mode 100755 index 0000000000000000000000000000000000000000..6c857603734bb2efd4cae30e7dbe196ad8a650a8 GIT binary patch literal 538 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 RHy!{0002ovPDHLkV1lMFS(X3* literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_manual.png b/src/main/res/drawable-hdpi/ic_manual.png new file mode 100755 index 0000000000000000000000000000000000000000..aad5c0a22e8ddb7fd001162234ca34f5828ddf2a GIT binary patch literal 308 zcmV-40n7f0P)?y1*FzkwB8L{l%lQ(n_74FcTyE1T%^T3@a%$&p^1Do}wMX zN=nT$5N@WYXos+pQu7Rio9QXqA*`g-JOklodW!b#2)_aE8bCb~P7`|o0000u literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_plus.png b/src/main/res/drawable-hdpi/ic_plus.png new file mode 100755 index 0000000000000000000000000000000000000000..82c4d36fe37e8ae58f9df9d9986d17c366851e9f GIT binary patch literal 412 zcmV;N0b~A&P)Mx!n3#U`8>=E02qE?(onHD3+WSv2in1nI!wb_%whCQSi+H8T0)5( zmQV_>VTUD5n|64N;XTO>ewr=5ryl(U*EfopZ~PA56#7cq$guV%oqx2XVGjW&Tn~sZ z0JPPS=y_;KC}}n9Fgsik;Gw0hjzrHxOF~Jj$??$I0n*$9>id_6wHBsU-^!NOF3M6* z!r^efiQ$CbAXbr3@D2hB~3|J<^Uy~1GH(HjpDbu9tQ0$ zdj4?E{rLBaTkH0$>uC`8t>;h0eRc0s`^?GnZ|o$(O3n*=j12xNA@8l=o$kj-+*NVG zh}1sk65RYV@IgZMtzdtDi^VD~7%#y|eIhu~fvF{(L_^RZ+*?Q}>1ch1bs@2WM`4t7 zj&rFiDsyu`LA$*oSj=m3!>N7j+Ul z;@N3#p4tyx_-$5`axX_gex=0kk8!kmrkf;kqU73ZTDEJO4XktV~ z0!H6JK~WLJT>O4K0|V*K^rSn(WV#!EW=>C4cl}j0-92^M+ahc2k@0Oygqt4m6yX%% zhIt^Rbi)EVi%yDgig3d`kW#u~0Zofml_`B6cOSN7&@lZ+s>rS?34emndJCz~^lxYq z;pLF|4VGC*P13&-{9S0$RYiCsL_QXfk&LonL0d?|o3L~Tcnp5!tkxVS!L8-54ypW<2Y8a zP=t>$!!5%sGEmMa(`-7&8RT}EflRU?2(M+}39!!Bm;(K;JlA{v&1aqvgl}NLfR7O+ zbZ;U%SVDf5s4rnHRBwT{9oO~~!E6DN{%Ti6NV@Ae_yAfSZQ~WAx5;f-H?~_A0%< z0Mk2tkp4$LL^IuxgnJnDxxuKH^zC*uRUAblcJ;qI=Hq{cOa+Lr24Rz*fmH`C?HYEk!7*j|UNs?((MTf*-Xz@>;cgk1jPKt;S>Fal~-#48G5Bg?dN zk&dnqFK0ycSBnTo_h(wsdtS-&kl(|V80da&;LDZ+J+ skgt6y!oD7?yMBsr-6P~{Uvv@v2a(o;JqHDa(f|Me07*qoM6N<$f~DVGTL1t6 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_star_empty.png b/src/main/res/drawable-hdpi/ic_star_empty.png new file mode 100755 index 0000000000000000000000000000000000000000..1d1c2bec23e53c45d95a9db79212ed72e3bd9d29 GIT binary patch literal 1441 zcmV;S1z!4zP)K3muIfl6pj+F4#F&t1&`$>CgD;B_HN-ecT;fY$9TSO_n6b4c z_y)?NQTRZj_e&_$(^G56{OLUd-G8?$m6}J_(6)v$JutU1U1ic8yR@hd!#CpIE=@Jvv9<(k? zNV`LQw~4M#M;eb=9Z{DB*b|B!5TL0@@bQudUb;2rk|a7UKvk9BaOTxu#!6_*6^6Ro z8KYM;NX~YoqJhb>Maf#=@KJ{+yJQe-K8=sqNUHqjIu>3xVG*yjCm!xl>_K95XgVC{ ztSKE0{HxAPcuN)bgyLTV<__djI@AhWzez;`cbW+~-L_@GzO9LXBJtSd$LU$(jC0~| zh&zd-#*wZD{h7JwmH|WEZlE+H|9*jTKFTiU3(Ef(#{D96z`Lo4UpzBCSpw{C|GtJ4 z#}H^L7^k;WQUB9|%-b4&ixX=>F+1*X+%(#8OUi8Gf`BonHpqR3)>SC<1rblFLLH%!rjU}6bF6=OcjL%(FKRBr8Ml`hkw0izOdrGf`K*YEhlbd^2BE-*6Yq3ri$FgWO$7T(K)BO@ZiE+ z_S}BJH)$^uJHbg=pYB{WG-qQoUN8h$*sOc5_qq#C`Qv}Wla474wjtBuKQk96y<2*! zxmBx4|Eo(mdRvRbkZa#5S<~QUI8fu}$RL;m~i%3V#&~V+J-MnFcHtCD`aE zgwLGl-gK8=jN-E6CTkhdVE{5qTziJ!q{N+p)!chIEY_h;}r%R{}B!^gY)|^;&x+-*@NNsb{%5s z|I}7dsQ*rq)E1`7PeNU6Ecj12;H$f0fPK&R)hcC@*!qbiHU}$_W&Qp*4)4Sc340CN zgUhXYy#nw7C@z+nSWoinM3SUhcmn!gL*Hf2T$!|hnf`a*+U=6 z?qw@BV;3Y2@+zmdCr%G1qyCvT3I;a?82|6<_&=Y~y~DzbeZW<|ZMGv>adb-(b;6lV zct`UID~~tRU4bbZ1%pl*FbOz*93NPX93w8ZB%=-A=Hv0?YD*Pi9z^cNU?*%CnhXc7 z4*jsGjOD}{dS|L)bnl#xV6~&%YB6xVG|rrLqmh;8j3hQ3SRCgl6AR?C(R%D(=bvg? z2i&sbPbIx;wOFkgmJA0r%rlb=*}x`sY|)st6)qYnOmf6l*rbjv8nd>-MI(htj@SyD v)Uicl)>gP^q%g@5TVazrwrI>=bqfClkM6ciIXUME00000NkvXXu0mjf62!W< literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_star_filled.png b/src/main/res/drawable-hdpi/ic_star_filled.png new file mode 100755 index 0000000000000000000000000000000000000000..82a400b773fe64297aceae8c8690dd62761825f2 GIT binary patch literal 1057 zcmV++1m63JP)#M6{xbk6=nvy1qiEBuI!>B!Wn^ zo`^>g4_<__yDGeB(R9WxDn!F}XRkABOH;e;?tI2{CfUrSbI!TvcmDVG-Z=y&*hMB7 zFF3$Ix$noEhv7 ztd1HDtw~|rQ!7{?9A&`+3F-!h@c_kVO5K!zhXL^5-Hm^nmK6wruY^K00T97^1PF;@ zBRPT_b_5s_B{D>0pc3xgFS3f4#z+%z5g%Mjf!xHnQ`9Dqqu+M|bVI-3zmuciA9GE> zN)#_2UV;aRyZFedO(IJ_2@fo5M8WqYOZ&8Dnt_whdtc54HhvEu%e5pkhHp2>uGQk1 zg>SXS*9BNfqICDO=|A$>+Zb?_)}%)IQ-J%EWAeJ*vx~my5mz_hWHNP6@`?)c+bM-D zx+F*7YX3m24%(Lt=@Czfp)YVMD~=Bx#=u83#K)RJvm{g=(+hMsjv>Iwb=?@=h}+W@ ze9X@==@@QDTg4XNjU4^5GB*qu0+jI%hLwZFjI1*GYxG$x@VOyCa68ujG7MNbl;DSc zSWU{5Br2qqs%|yD}?Fy;|c51cnPF^Phh*)QrV`;cN{0ZxFUD1Mu+r#N&3_s|UZH=BX>z*j|PJm+X07Eax$O8HRp}&ASHW9j@^Ucw3y# z&IR$}_d<2Q-q3~sbB*CzLg}qh$qryUr%CF0-3Fbta`IhG2g{YsugA8J($p&)X-^4R zFSYnO{tIC2OrFE_yEB!na)`J}6ZAl)maNjcB@8)*)!%|XDG}&-T*tfq6X4KOOtcz* zK|+~^{?>{|*(6@H|HU%gl9Xxg$6pt&7_bf40*_*YZ&Acn12a#Sq%croV| zY_gR*YX^REHV;95=|^F~H!XmpeRT&`+BtH35*uwT$M0A5mwSIV$GU#XdbfRa2Ud@0 zj;8}`PMxhA#ZlP0+M6@7qp&%3wr&(hVe4vd&d83!=G57`Q5=P>tGziRI|`dqXX{3p ba0>qdI&2h7%kas900000NkvXXu0mjfPbl|0 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_tag_black.png b/src/main/res/drawable-hdpi/ic_tag_black.png new file mode 100755 index 0000000000000000000000000000000000000000..5d300e7017bc1768e88b0b47173dc87bb5649cb8 GIT binary patch literal 508 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw1|+Ti+$>^XU_9*U;uw^9PUj zMoQe1-JM_dY<7Qb{`R|CZ!*N6ZC5?KQ+-{_)Pgk80Pl3M7xva(5uE8^j9W|68ec73 zIxWl_%t=}hwnH_6P2y#9vdHx-qQ7OI>or`rt6u0?u&l>FEc4XIk)i&Xjn1$Nh^HJ5dvmWZPyxqB?w&&aalZk6< zLSOFl{bQHu6fyU-$Bf(?Uu2nY>wRHd~EaK>Xnj^!_TtC zcCa72^F;C3+&R3nxAPS|QQL2*$58I;tRWPaD zd8m1;S7rMQmBXAdGCx?t#&#Kj`xx-76>d zo$yxEN6J*VX)!1ONYZf_cZeecS}z4;%x6})$rmZ%wDyl brJjLJKgw3fq-+W>RvA29{an^LB{Ts5&BfRw literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_tag_blue.png b/src/main/res/drawable-hdpi/ic_tag_blue.png new file mode 100755 index 0000000000000000000000000000000000000000..c51c0d088035532145682273d91df285ed3a992b GIT binary patch literal 607 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 diff --git a/src/main/res/drawable-ldpi/icon.png b/src/main/res/drawable-ldpi/icon.png deleted file mode 100644 index 1095584ec21f71cd0afc9e0993aa2209671b590c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1723 zcmV;s21NOZP)AReP91Tc8>~sHP8V>Ys(CF=aT`Sk=;|pS}XrJPb~T1dys{sdO&0YpQBSz*~us zcN*3-J_EnE1cxrXiq*F~jZje~rkAe3vf3>;eR)3?Ox=jK*jEU7Do|T`2NqP{56w(* zBAf)rvPB_7rsfeKd0^!CaR%BHUC$tsP9m8a!i@4&TxxzagzsYHJvblx4rRUu#0Jlz zclZJwdC}7S3BvwaIMTiwb!98zRf|zoya>NudJkDGgEYs=q*HmC)>GExofw=92}s;l z_YgKLUT5`<1RBwq{f)K~I%M=gRE6d)b5BP`8{u9x0-wsG%H)w^ zRU7n9FwtlfsZSjiSB(k8~Y5+O>dyoSI477Ly?|FR?m))C!ci%BtY!2Sst8Uri#|SFX&)8{_Ou2 z9r5p3Vz9_GY#%D>%huqp_>U}K45YGy__TE!HZA@bMxX~@{;>cGYRgH~Ih*vd7EgV7h6Pg$#$lH+5=^lj{W80p{{l+;{7_t5cv3xVUy zl_BY4ht1JH*EEeRS{VwTC(QFIVu8zF&P8O$gJsMgsSO35SVvBrX`Vah$Yz2-5T>-`4DJNH;N zlSSY8-mfty+|1~*;BtTwLz_w5 z+lRv)J28~G%ouyvca(@|{2->WsPii&79&nju7ITE6hMX4AQc{|KqZN#)aAvemg3IZ zCr}Y+!r}JU&^>U1C2WyZC<=47itSYQ`?$5{VH?mtFMFFExfYTsfqK%*WzH@Onc#i` zI@a|rm-WbKk{5my{mF}H>Duc$bit&yLAgFfqo2vVbm~?FeG#0F?dSP*kxSo0Ff!o@ z(C}B;r&6pa-NY4;y~5lX8g&*MYQ>yLGd^tDWC4(sGy$Ow-*!eh%xt;>ve|J1q$*w< zh;B#cz!6l2=5bkX#nJ9PJQ`ew8t>7z$bxqf*QB=l2_UB$hK|1EIfloN-jQ=qcwChF zYAkkyp=;FwcnUB3v0=*tMYMA(HdyBa~+x3uKLlN4G^pb-mU-V{dC z4c>%&c?d=9r9!FoB*`JMC5g|3-LOrwo7rr}=CFYw!|c5CKKuKx zz`c-!I{|1Skw|1v@HsI$O+ct ziJKZW;3*4Usn_e{x~@N=|5PM!0~n1)$B6w)*cowS$^gsd@+pe0nUFS#E?BM_ux&9G zO_xfg9|Ec$WVgCXEjBj^1-?Roi{qmGbAs#!RJutl$4rsLaoAcG$WGsCcNd`AW5#+= zbKGP&$961$zvxG7YVkMY%P1cqZj-*6gIXlFeCld^y5@?-;ukTF{qrOjw%bAe;hFdr z>&iZL7PZXFLXBjP3?3($}q_W_7Wc59mUyo>P4VeFeQL;5FW_L9Yt_0|D11IO~M7kN^Mx07*qo IM6N<$f@|?RZU6uP literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_collection_black.png b/src/main/res/drawable-mdpi/ic_collection_black.png new file mode 100755 index 0000000000000000000000000000000000000000..32efcd7b7210af6b9cd2c790bac23bfc065d51a8 GIT binary patch literal 275 zcmV+u0qp*XP)j!BWe4P0?%&E-QBbzTj{~@k=>xf(1^B9o_YP{!o7}8H3&5Hm3TXA4 znXE1d^<~7Mr{5xwuD*-__R3|I5Q+IZ+&eAG zO73Lz3ihDUHA2SzH&w`S}B?lv71vj-lz)quWhenUiA6Ty5FlE8QGXa z*nFjAm_%QD{;hqnJ)*&s`Dd!YSwAhV{eSqB((O$aeUKEC+xYV`&#JUHRyQ8p;CFkk k?rGQs-`tv}xmUKO0qAK4Pgg&ebxsLQ08avXl>h($ literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_delete.png b/src/main/res/drawable-mdpi/ic_delete.png new file mode 100755 index 0000000000000000000000000000000000000000..76ad11372a2d2e59068c11dd7dc36281d89fa229 GIT binary patch literal 271 zcmV+q0r38bP)-_2Uz34An`XD( znSBhC{a9{v>b+{|e<=J@OiH}BxlNX1vfrKt>t?+K{ewp)9P_T?O|%dax->s~e@(gQ zwFB~o1$NzxD`k_Kb0%apM4dN2!M2iZ#aEl=oDcIEua~-~M($hr|Ig~{^A0L-g!x{6 z^J-U|xa-FN*+-T4u55cdSs-b4>dnaIcW*W9d;k7vqx4TZ)hBznZyvgvVmI-<;rY)y z!VhS49SaSWU2;^_!7I4i#P>aeVB13xDJ fHhhF#MYH^#2_>47G8-lX!;Znz)z4*}Q$iB}(lnQ$ literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_edit.png b/src/main/res/drawable-mdpi/ic_edit.png new file mode 100755 index 0000000000000000000000000000000000000000..b65b2d2ddf9bb1f39e0484ae71aabbec461bfcc9 GIT binary patch literal 392 zcmV;30eAk1P)7W?o8gLwEpp-h|^n)N+V!nhjr_~O6o;OUA z*gULl%i&g-&xj2q{uyp|B8Za9J=n;lt_h*1c ztZhU(1Eg<2zHo1Zd-tCJW;+Ln*nnK&8f@j^^h?whaSmK6NkdL`+-0qOb+*c&?3e;L z{U|xsS^;gsu_Lw(#{z5ut}@_f`8ua7M5dFh&Sm>e{)z$D$7*nC0*-93X*e<u_X1Tb7FEC3R2$a}0<< mZY{*17~~q@j+YK{4ZZ=am%u4Ic&zUL0000 z87Z&tt97JP?%C_CBRlO~U+Ze`z1GjSUnigQyyZRT%LPE|3Izv)gH{Ku^TQtW&A~na zI=6yF^Rr&!JxM(7BVZe#ssh%9c%KDimJTieXjz~Z5m$TyYy!*;)G1Ik#BS)-3YnFI zLjXC7!3^W?r#l2HMt4N(0FJ1JD7~;#lk%<3^!V z$MIjo(gHF;89ISF5{llUe-OIV!g0I>kn`!{NJ8O5aW4&@8IbO@Cse+3aQVj+76Sh5PL$&DZg@xZB-fwKYFB`wn@atw@O=sMLye=wI^bBztiVgw~&?eL` z{X2-fp483a;1#OzspF-)3w^|@Gxr3<7lWT(J!(3GgM5ouA-=(n35AR3UxMbLUwwc>mjnUO#ie>>7s&4GRZ0000ra!_luuLeMNY5aX$u>Jf*w^%cGRzpBzY%X=@1IxOW|8ydt1Y9T*yQmWcO|{ z)XPIK1h?76SEmA~uY4cJt~|w7sUE=;B2)qK%3(nIKm?02!Zz=@Lpg8TnxAYB3M}lP z-q@)Sp}_(wf6fdl)QkVcvx7*cRT^6osUZvqHIg)Smy6PUk-|sE;8WWftw0TZkvigl zy=pV2TjZo={+bgJI1tk%fQ1c1X%3P%Fkn!;VD8?Kz+dM_MNrK3L7HtDqi+P`|As{Z z46s;YZ-x35bxZTV~m5B#vx?3!K z?ilf}wxRTO`IsGuwzk6ZPfe0#OC;O(JiWCrVT;869~0R@tfcyIA@pgYeNOp>u!?}<^xeaDDQxk z!HM3*QC6QsPUJ(9gt$nJ3{b?S6KT|Og+=qTEf#g^sJP6*%)z)0#LfQ=ce%1D!I!Vz P00000NkvXXu0mjfP%cG+ literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_item_black.png b/src/main/res/drawable-mdpi/ic_item_black.png new file mode 100755 index 0000000000000000000000000000000000000000..43b240a5a793b383cd73be8af0e8a6938170910a GIT binary patch literal 304 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 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_tag_blue.png b/src/main/res/drawable-mdpi/ic_tag_blue.png new file mode 100755 index 0000000000000000000000000000000000000000..1db6060efbc647ee5ba6f1746630c66f17510908 GIT binary patch literal 367 zcmV-#0g(QQP)_45m$^|%~|&;BorO#h7TW$@o7)sdt*aN6hpAPeqi z@e-|1VcO@VK*dqOfITI|%x!Zvo$)s*jwIS5kfDlXI8bTkJO2NF*)A|J{8s>me(U{A z-kL$>K#*gUrhc;d&&YTH;;{dY_p|s86YEHthak)$z|e07vVoz;_<)i5m%+Vsp^wBk zl5P$(p822k-QTZAfFWoGOvuX~WbsCiI*^zU7!pxDs58l+FJ|alP!Im9Zjd(?NXfO- zQcF@nO)X1FF=y0)q=XhWwWA?88iJID0Hyjz)8J?bQW^r3>L2uJ5CC5Kk}2jWMU4Oe N002ovPDHLkV1lims`>x` literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_upload.png b/src/main/res/drawable-mdpi/ic_upload.png new file mode 100755 index 0000000000000000000000000000000000000000..4eeb9ba428444500bf8152f77b597dce3dc6ac28 GIT binary patch literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU1|)m_?Z^dE>pfi@Ln7SYPIcsJFyLX){TF?) zKf8r1XvHiaKHHpirLz;Il4i24H?p{|;_%DKNTc}*!`W=M)Cp1xeqP{_3!8ZC=&9Mo z?CaPLGgs+Oc<;QOYx~vH-*rxi9AJAVHi4;=(YhdW0>=kt zJys*0AHsajZ47e@IE+|stebG}QQsfSTf133#JBOBQZRM+tZD3_c<&R_lCsnQwv~*t cu0urC70x?pZWC|F13I0-)78&qol`;+0OO-p>Hq)$ literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/icon.png b/src/main/res/drawable-mdpi/icon.png deleted file mode 100644 index a07c69fa5a0f4da5d5efe96eea12a543154dbab6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2574 zcmV+p3i0)cP)Q`Og{P|8RRXpj5bgrSmEzSMfBn+{{vpNxw?;5UX;iv9sYxy_`IQHs$i<61a_iv^L>h8s-`D(`e@|IgS*Fj zNGM876Gf;3D8*1UX9a%v>yJKD*QkCwW2AirU(L{qNA)JghmGItc;(H<$!ABY&gBy1vJIEUj-b8%el*o|VkG)LqNx#TG>Jvj^jIte!!+RY z)T4j$7+PoF1AkRBf}R#^T=-q|PaK1$c<4UH)Hpq3$4WA|xtr!ZQLC=*vNE>O6E9kp+5X0eKB$6>C(lPwI@3#oY zhS_%x7e|j!$yG?ECXmh~EH~^OeuK}+sWoJse3Z3?ha3n`MM9KvA?uqpEnBg4Q46)7 zM$p%a$@l;+O}vfvx%XjH`}a{(-HHth9!JaUwV0*VqGR48^gWNYN<&~7x)y$e!X>e` zZ5!6KZoxbKuV9XUDI%#M1~IVh?pNSdeb~6@$y`v|yk=XK+fHxnDqnUK4&=QRNyIVf zYbDM*cI>~qIy*a7=z7uqkw@agd(<=y-Q7L!ty_23SGdXmahO<;N=wB+j;lNm%=OHC zy zU|>La6h%92y4IPufI$9>Xu!@y`TaNgtg&41@PwMwBdmSm7)xAWDLoqjZ==P2#*k7! z3o1)cVSI3KP_!?d8G^Lg0FtLXC~JYdxi|c%h~lXEixY=%VSFF@!*3&&9>(Rb|iK54Cx5;s~PY5iaV1het%w`dgQFBAJ;aFK zImQC}(|QaCFYUm1JVfzSc)ebv=)ObI)0jwJb``}Zj9J0n0Xgn*Zc(rFM9$xh_makZbm-at_v5^SW zM1y1SW@%+FuIy*WR)i3A2N_q;(YO`O!A|Ts^%z}9ZepCj3ytlw#x%N_fNrKKtPh`< z|1{UqF`4LxHaCQ79+E=uUXCOZ35jAMRz%R%0(P!0FMv=sk>Nr8%+OzY^c-M9@+fz=G`qa@v4sF5u-2289-#$**LWnyNNDwDf1( zkUiMnw|y$tn>pQP=Vn!#|17L^5AGrjtBkN$D@v)Z7LXc5EFhLB4<;7Wehh)CMqX|W zqsiZaO^benJ_hwa&V0ub$-_HUk**?g6fm9|!@kguU6*zhK)$qn-<3*kFrYPIaqR=V zUaUvk>@F_89b@tHs8R!*QKY;INJ<2_U+K6Ca3e9Gsl2{qY0%a7J?uICWgHuLfj+MB z=GkAN1&ifT#2u}B+2S#~$5jA(Qn^;H%CCmIae4AE-Dsng|Hl*Ov!z72k3ZnJs{pp| z+pW`DDueC#mEWOf=ucJ!dTL}hzOeiS-i?m2E;`EKz4<&Lu~NnW?peqVU^@<+T3KKu z{yrI%Qy-Z%HEvLUz}n^~m?7x`xuCtNR#L2En!T>dQtIKdS#V-Hzt3RtwTeYtmQ&dR z6qXZvac*oc@BUYEH%@Ylv_1&tSjkbzzU6*h1(3^C`;1z;g_SmOtclS?KWk2VYE zM*oS<=C483XckW?GN|1jfh3Ro(hWh);J?t;edufF*l0@Yf@e# z!(xXsx&jFz9XnZ>dYBBiJ2V6;JQKrEyhr;%vi-gHxo_O}T;Km>`kocn=Q8!|H^-qG zXf)r?!odBuzklYezBhOJUT)@|$&4Hy79ZHO|HGR7%Rj8SF}?lVs`f)~QllTNx$*Eb z19o?PSl94z-?XRGV;>kb{&2;ih_K@kKApdypdipW^+hl+5E(pO{an^LB{Ts5(>;mB literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_browse.png b/src/main/res/drawable-xhdpi/ic_browse.png new file mode 100755 index 0000000000000000000000000000000000000000..87f08c37c7801534c884328edfbd4c67eed7e6c2 GIT binary patch literal 1520 zcmb7^`9ISQ0LQ-@oBQ@c8zG^PV=S6st~E9+N4b58;(5%SC_K&_Im#_bsK;4iYKlkG zk|9S?B0^%s=7^l-sXw6~KJU-#{d#?V{-l#_PYdxQ_yGVAvLaa6|7!B@@WOvFTWtLS z0KjNg7N(9-p6pULf60rI-Bem}hT9U%QuHI|U~c01{egMs`EiA_&dt2NKU+RPstDOHO_3 zDvN>}Pb|MaHFe5lP@ff3l;z0mIwIDrvN>Z0Syg@>os4p>`xU3L6KVj_fSq9+@~Qy&&&JfG+_<2nrDZ&ctiLsCN9c^tt>`}#@;rU=K*q$ zIP9U707$SVc1MarBp-&@tAldaItor={-Ix90Y2y5-3@v$B4qY1tS4jQmQ{7zgZYa|uU-7$tX`*-8t$e5>OP ztAb$Npe^yJF)p2oKbKx51bTQ(x)>~7-B$Q3X6#y=!@^eKTv?RV?sqs=T;<&ksySa# zk3lzFw8OI62;T*61aiF;0oGc2Y}EqJnzFp^2X0;Q62KXRSQp56vX5g2AvLIjVD5f$ znnmQZ>gwu#nyGLi2EfnIrxtRgxMI9y<42A+9|Ym{dVT2%5t`jo37R-=VbOZErKY-d zYH-*f_e$s3eObBw{(h~j%*-MFff#cUPj%#@jPRtYQ8lCi&SFX|VqJ{EY|D?i5zvT= zAZ5Iex+NW~zkyp3Tli62KdRBlHSHHcJ{G2v*{t3EsmGr~J^|+fwx)kZ59^*AntBq( zL(p15t0O#0T0xJJ~c(|WRxlWHVD zb8|*zjPXGZ-v`lMH!=t42Eo_bs1lM227_5JyrOFJdqgA>13?gM4!$(^#tZ8R)YsR~ zvf1othJQ6-V_Nb7h6a6EV2|>K8Tj#b`!scVcz7J#DieZMmZDS}%%iNuO{E4v_zfyv zK)SNDBImdt{IS=`Kr3JCLrEH~Jda0VZ8NVD7T}EdqZxjbVO)#Us>nIUn%xo)RJApX4z z!9YraX;nJcV6wV7?t{ddLLK{DS`eNengoW!&ZaODzbXP)2Va@touX*ZO0v1X#mV(3oe5a`7;Jsfc5`T>;YOKfgMYeruVftot zsjfS5_sdPjzxF1w?snnl)sJb!KYXRF)GhGMUtYV>d>-90YSTP3;b?E3KG5k*Ltll2 z62;Q06{tO|kSFI44^V6v5~*36ae^$0EyJaAE5CrvOo==1kr09z*U418Drg($Q&jff z1wUjwb=E&M(Rn%KjLDq}Z=!DD3vUG#`dD}?Gfhys`(sV!&K+xUi+QbW5?|Lhw0aJ$ z&XEtOIwSFW?%$Zt zq1OqVK8Zay_`Dn58l;~QieMM&S*Y_-e1U5Ce51K-pH3oIW6)1WX{bms?s+;yi1Zfc-h9~ zUy9}tRmS{B7oH2e*S&XbmqwKK)-#hD`iPm4}^GuhQ!I+y92#5>Kn zTXM2}pPmzA#=a*yOipf$-$f?edH-PIzN78;#NACcys8W5wS2PAf@#C2y71nJx_BAJ zh|2w|*>v{n_c6TL8~5ehY^OJ8zn`1^bpy+squ&ID0uGcnt~B~{GhD*%_q!bpcSG#` zMFJc&7#^D;k*o6+Z0albe5sFL+U;9&Jy4H9T*tQfWfIdNckKlYnT+udO>X8rFlTVD zaeH=S_5NcVGb-;GmcJ18f49_Ee%E}#w-UP^{%3e`eU|aOPtQBz*El=W-(qBp-lG_I z)S`Rh-;WgskH6R1xJzCD%@4q&#Hdj{=5k+zYvV#b)1(^0R99b707_>bn R6qpbgJYD@<);T3K0RUkR>=Xb1 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_collection_blue.png b/src/main/res/drawable-xhdpi/ic_collection_blue.png new file mode 100755 index 0000000000000000000000000000000000000000..4e472326196f91c51df569c18b3c941dc36f9ad8 GIT binary patch literal 596 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdoh8V3PK9aSW-5dwa{#kI7Kt=*Meo z1^N>rs#vz~GP}pD|3L5yLy^F@b+%33j|6!(l&(qiFz|R`{yuxwqx)-XZ~S`}%4KZ$ zL7CG<)7OGQV;X}4gDazk$iHmH-if$0h;}fet;FaY`JR#;6}ZEj-=OZn^25 zmJ4%lUlY2n`T5t?*5m!{A9w4%Hs2M+Y+mtDSNEw?%(--@J?mN{cngf8&jvTYbJ>$y zvu{S<`?+zazZjV^hJVYg&d4p?JEPBS*;H%ZbsRh1lwF@7l``ve%Ce_(|Gc_;Erjuk zO@m3m?I!iR4sUCoZ{4|VeekzE8NZIt-E4CFbL_eKn|m~i;!4;{?CwW46dl_&U)-C) zYTo-IAD!es=et=MKHrnMV%~VC=AFgq)Elzg4+3`Du`4u4H#*C+`j>Hci`!ZAC74~k z&&VA#~^E32_L)n~G2v-kPsKhyh zozVpiDGARR3Z&H}5+0cqFqE}DVYtK8`R-xAOZy^$t5wPBPWj9E{>l7)IsKT|t)Gre zoJ)D%v$UuR1UP6gJT@bh{IH+F%(qy+y3(f5-oN&~{Tz#JFE;p^7zn>-*u;4xtD(_E zWbL8#Cl@aa{TB9A>G3_5mal>V2Rs|Iwi|r8pA+No<8lbQ&foZm*DtFJ#4oVPao=#b z!NJHnNBn~#leE%;26ra8m>JDI)DEtV0oVN!p3ZdeMc7Mzc}>&#D2C1xeIry zFJH@dW82%O20ZUK&gyP8|1~pc*Q;Ick{?#4FJH}PvF;iBN6US2`UTeSll!(G*uA>f x{9UT(fmytowYrQ8H#9IgF-mN2B!~RZ7WQR&pkI05E?^`wc)I$ztaD0e0swii!gv4x literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_down.png b/src/main/res/drawable-xhdpi/ic_down.png new file mode 100755 index 0000000000000000000000000000000000000000..f14f8dfb0c9cb35a2ddd923cb477bf71c68c44ee GIT binary patch literal 618 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdoh8U^4V{aSW-5dwavz?@)k5YvQdo z)fpDeo0y#vydqeHeLD&h)r20m*k+V)tYIundn7fh!aFNSI_h;T@Aq!GWnb=W^FLqy zv(u_dv5{k`=sOO9X$}ocEldXM*ti6^4(yQ9aL{0QoV9>qAw$pVMoJ_qHn8;_fBd;_ z|NnhaYmJKdKmWA3`Tl$Q?6c2$*F2uh%VaUr=aSB8xx23Ryll2h$==ok=y~}pvf!(FvF<~E? zAIL_%559f>go(+=dv80twUQdQmptFMp-Eh1!^+Ngd-XfQJ~lCLb3D^u7$17Z^nEc` zQ*S{f&+KD6EzVuJF>U=_mIvm(Yw~s;v@_^?`!|gtT4~lHzEKiSwD z%9VRgis5TN%(F%C=vDI#2TpH#bM?%&&{-e%I@**su8=C=zIW;(=c1?s_8;abu3~*t zwp+YV=01m+)@t7n?uY|ltM+PtejU|NVJ+9s-c&fXeFc-ww6{qcJ(gX5`8jKu(-D2< zijYWmfdJ-+Tc7*R?m5SEVzS7^nEZR{lRdRQtgvxmebC1Icr(+ZGj{}kFWdV0lvR_% zhX!lEXwF5Udk&drI2;U36<<`kX8|K?#cI}VoP`O~-Y#C78Xb4j(QE_Ly)c)9-%2Ma z9%Ovi_`N&B?{;wUt$i{sZa>_cm{>k^y|lXd`C4o<j+u+w=bSP8DyvIg`7;@W*|8wNdwH zKz8z;KkbTt=Uiia-IHF+d0qCr)`R*3o4GRXO{=|9c)s{sZV%&`rP76&F&Eg6E&CZ^ z?edKwq&>ZwGd%Obo|`$}+!tTW>AzOh+j7BSYlWgofa$z^hp!cOjMSK9J}ej)Wno9hnF;tzz1oix|)Q5+xO=H6C#7BtDnm{r-UW| D!S5wS literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_favorites_black.png b/src/main/res/drawable-xhdpi/ic_favorites_black.png new file mode 100755 index 0000000000000000000000000000000000000000..229ab0b3ee32f873cee02c9fe43307c19506553e GIT binary patch literal 1132 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdoh8U@`V|aSW-5dppy;LnKh3t-N=J zLBcVfuBL|zG`QGYzAVycYvx!aad3_w+mQkzb%RA5j}A605_e@06mjKe3lKRo!!Y5{ zoB!VHbv9qVY#aOamuV1x*UOjdR)zoHzSZ_`-1KQT1OzoiU84&+8AZ$*8BH3Tm<|=Z zc3{>}QQ+CmM-unXdB$%%Ut+~xm59yz%6CUzL;ihFgF)dmMzgkU?v4Axe$;4Qd8J}= znnB$8<0^-*LN)8koE&DDtY%p4aJ@F7+u^=&&9>IR0}2xx(9e}jdtWV# zWR8&M`q><4?DF#YgZ8IutvXvcH?tKmmaOT$uu0(hb=51Y<)*1NOkMC!Z%u5=sp=i~ z(XI=^FOqQoD+V_!;NO%roCclfvT^z`1Q^LHM{S)5mmo^w$3o~p$3 z@-5+&eU(T0XAY zw}m0dJzZk{-6(6ZuL3`R`cz*2Xj6ZAIp>7Wrp+G{ckWg$44*ii|K@p_r|Tz}Rn z5POrL5E88Z-yk`0zR6^_r=J)nn1;6HHl6LzGI;hi_rq-F-;$T(&z==`;Cf&kuXo&p zZF*zeJ=xzZ7W|8@XJ2sc`?-PFYiF0&W8nvaA*Ys3J@(*I;S{$;NyR@{e#jg()7)V{SeLYz|0!PCv7k0F4F2Pd&eN2cVd}U4dWb!skgj!^d!G8_2QrWlWB)^adEjy z)*TL+T`rc7tjg=vj(-R){mA-3ETlwd!4u|ny6w4qHEHbkUyF3??9HAV^74qsGiHVD zUn(#2&z`+nWM5_Vtl;|Mn&;Bi>&!m!=7oPWWh`G9EN#cY*S;&cQe=~M`k`}fE95&~ zM?3GzWjQDB-y7GcR}gu4)`G*Qp8e=(p1PrTL%yNuJ)Bh22Td0t!E0 zY!nZue7~UTonVpZM~j8Crgc2dYOAi75l^fYdgdiE&Fp~Gichj{7<0tllq_1M&Bl9d zrS1l^4OJWTy(*P2FX7$5t|i6vc>k<#+;aO)JnBEMd1I=_Q!lr9>nAMz6! tco$4)Ft}rNfq{#Yqzi(Fk^!H<`*Syna5ZB^=bOUb3(*wwEbpMr6X`=t4Xc)nCEP%AkTZ_tJe z`8ioE{sVeiC=<|{k2`P&jOrlN}HuCgHC3vH%cAeA{Ka4e2bDhIHDYnU^^ z_@w&LtVXeVOnNO0ZT4)#Sh+lLIgpQ?XTBaHqb&{MP$6Z^hA{11GW1Xjl2P&M;k!e0 z+pmwX{Xg?3sp2BY`RVsyukPc!8G!OzKd=@l+$me9OfK!T-B?_4&icDiI&hjP1I9Tx z_nay*akXx0ENrbZjYmp$ONPkBUUkOz`#ZMUzHQQ282ORV9<0MN6`M$oX6I>pBVPX3 zfnpO;q8F|yT?8nXYk_#quF>ewe@PSmszQ_6n-@_l8nHRKSbk{4bZ$t$AEmd!FVRX0 zwkb)(%my>sNyZU#j7*=~DqO5@wHvC>2utm}f}+ z>qNB%bn8vY=lu4IB7T5$0lxe&O*A37r9%|4;G*JTGAvpL9Ec8Xg{kJqfC+tKDTY7$*Q!7#Xjedm z646Nn?pX|jH>anS!om#n=A?V8m`P(@M z0`nKHgI>?#FAiMG2XM*EyLPKWADQs}rxx@&LAwomI>X^2Jw#Fl~0#(cTyp+ASi@0{QE0us7{gCdtAy(!< zaWOx?ut{B$hkx?9+-4d1#J4v*i*zd(z#$_Cg$2CT4YZnD+U+7q)({bsB{N7eR{eU^ zC2+N`yYZghkHEs04}bv5p#&}J>9=^}9=hg^93?=KFNb_TivV^PH@!m)dt>bU`@)?0 zo86j>4B^R00+W(Dq7iy~KDWzGm}?;|+yq}uv&-Jvtl}V!xF0E__yyE^Iva^K=huhq zOTKo!ux62Z=pfO(L(G!-9JpO94q^1lxSf&gR2cL%CZ%RpL5c3Oup zS90#jNKDrVfEN?VGx4I%T62i7VjE54lfyIPak>c>U9v(;*cnE)a`k4(81F6`yH~v< z_~x$nzztNzbbYKsmG!wO!WH)#-N=wIXkBp35m>vnV~o&DjXXOMz&k_lnWW>ouQ%t_ zOS-`qRh4Eta+b@vHDBJ#ROUHWX%SAQ(ZXeeqks=0UTp3AT^`uCVjEP+By>+-r`4`h z1eD$wqcW5n(>rNx3bjx!36*L;j!vRaFTV#VS#tRCAa%#k-pul=eT&0**G>9O{L~2K zK#bI(G5I5~dR@TTN(*HX*@6Yr#a)P@?(}i?ro=(2{*(P^MCnBGbR*@kTtRYm_G9`d zdsCkRoCRrpb-^sll}86s5u6GwXN2 aJLZII201seGP9qE4IuSR^=fsUGSJ5)dh7)PF7PE0023p0|NbhUH*wQ z=zC`=eX9ik$#5jX#{ItUNCaHv>?mA(;RA!EManjkiW>^eb` zJwQZ>qkeMt2kt3Hd@L2;SDa0BJ91D1D3YLjtLy?ekh!OqB>(+dB(}xlWdsuVfi;?$ z%!*oOn#iAUq$~WFq=1S5sn;tLp|_3kTdIs7nA_8x3av`u=XCQAcqq-d%{~KH%S3NI z=rJHmfQGk=CQP{1!8*7DJDF;!&I>|YE=9c4uesu?!8Za26W8&m$U|}7h`*>f&v;8t z+qDoUCr}-sW{{!q#rvT&mMszaxvuoYndh_Xt@3~$`$EbvV^vQt$iX68BfU-W(}CvK z3ZM=hvFLz=AU7fhcCksua)v{S6e?}0M48fhCL3W6LN?D9y=f_kYsBvqZVbM;W3*{( zxX-ID=>^?YI|l;=miGd)2f>a?UFwQ`r*-wtKB0Gvn9fn?zdLaa=ULs`3<2Spz>4ll zif0Swf|dDRnjf0(3Js$Y*ITuzjGU!wLfxFtEMNoqn}2d-o33vK!ke*SR3mwPDV#nA72#yze#MJ>1b4g`o+2dtjO_S2vRx=U~3k~DS- zT77E8&`4G*5`jd`R3iMMx-A#|Y!9+;KspIVvbta`_>e1rJP)%0-i#T%O7JghfpZgN9UQwN_ zeQhV09Cf({6^K$Xb1xwqI3r*3o1<|7{CeTd<{@^D-%8#|jx#U(c_ zR}bz5$&>$Z4t9@uNy{7FV{Fer_^AAh$g$??Y0PkZ?(+IDs7K$?jZ)v9Nm^-gjBY0^ z18Xw&zuw|Mo@I|Ny*otCo>2s}BnM2VF{DWXFK1M}5!@;bCQUZfez5H*+*Bg?iDlNlt_v7pcIV^0XO8y14~rg#snR%4KTJ1{(FnYh}26zX2zTw#Wbg literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_item_black.png b/src/main/res/drawable-xhdpi/ic_item_black.png new file mode 100755 index 0000000000000000000000000000000000000000..35ea6c81359f69fca99e8604ae7fd34f568d1afb GIT binary patch literal 613 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 QPXGV_07*qoM6N<$f^BrXNB{r; literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_manual.png b/src/main/res/drawable-xhdpi/ic_manual.png new file mode 100755 index 0000000000000000000000000000000000000000..0c72f3291ba6e3f8bcf399a4e3ac3da28e6db55d GIT binary patch literal 365 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdoh8U^MY`aSW-5dwbI{uh~I_)p5uF zg}HmaGI4L+FR}2Gm9NLy%?_vb{ZwhRz50kZL59(xAyDC&7>5DZkvmL`ZjCdd6%J?! z6mzpkuqNd-GAwpDqbrag(y^13sfWpMJBH#1uNu}r7JhH|^!GI zHi``ld)bfi-r=^W{Hz^wJJ@0#6WG4T+<(4jpZW5?a(xxk9<6U*KQn;b*{^SK@BSyt j0~Ib{E@{q2_v8KKcMgH7{!ya9U}W%g^>bP0l+XkKzs!n| literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_plus.png b/src/main/res/drawable-xhdpi/ic_plus.png new file mode 100755 index 0000000000000000000000000000000000000000..8e358f9dc2142108a34efc81c2118e4ec8c695ac GIT binary patch literal 482 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdoh8U|i|x;uum9_x6^dA5)-&Yhs*h zs^=l58%%3CHx{!WkrlAMwt#iFf|;4hu_JNcAIRmb(amX-&dsPl*X>xe=DuXQ-FjBx zTPNgIE(GM-2nqB#I4~|`I1{^|A%NkTUVwuJL$SDqf=I(oZV>^l19w=tI9LfqTFYBBc(T()aYJ6>ThP7frW}%M_c}>*@E}qBWK)e ziPvjtVuJVsT`LZ6+i=G`5xV^-HSe?A&z(0)?C(9@ZFu+nEBV*}habm&;os*u We+dkIta1eycMP7celF{r5}E+-y}Zr< literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_settings.png b/src/main/res/drawable-xhdpi/ic_settings.png new file mode 100755 index 0000000000000000000000000000000000000000..096aed8db75dc8e3ddba165feb0e1244a7d9764e GIT binary patch literal 1032 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdoh8U_S5Z;uum9_jb1T3g1APW9O$H z_c&oZvr0tGOQT58xMxcGT}H{o0}RaK%pz-$mPTd<(l~K zE1GM+MP}Dm%l?kLzixf?uIK;%ey{)gu73OVxazxov-mq5+MjIOnbwdbEU|#Gi^XNP z^#ul3rbTyUGa8f_FTUe5ahTYUQO+*4z{6q7Zf4#96@^=O8`(631m3=LU>4!*C@W8p zXReTcJo93bPtQ!%zTGC(Q>_Xt+`gI>q$mq~*E2B7jF`%B?uf^G*1l=7C;a|Acc03z zj;(icqsujgcOeNmmo{iI-f5lqjzi{c-jP!FV`{vsg$`VnczNo;#d(f*RvoypB=If7 z4i067InoybUxSe?p+qvvfc|6y%{7i4&0&cAv_m_07 z|M9nBnrIvAlc!0%Z?34gdo?T${AirvG3nId$#pI-Twav!P_;2qb%kGUSbc}e@Rm+YV4t$45SIM>d|+o}iuPd9aCFMFgDuHcuL)W6xFfbV!^ z+_i(1O3x>0CkZRh3zj_RnJ@mqZ0iz+6O#)QAJ~}aEs8kw__FJ2?;g|QBsu1fs*k2S zF4cEf!sL-RM`nKWGggNusn(jyKmYSN=vTwl8gtHEaJx*EaDaP>hgXBnGW#v;k5bIu z$K3w#ci)Lu+m-6_``YK$Fx=v+?LH$bGf(QldMAs{v(+)Po8Hf73BN0BAz3x&@k7hw zF3)=S^2HtOvNlT3mTmsv`sTA%e$f2lqD`+)RfXhKNip}l+j*b&Ez=9{9S`JBnNMtb zkn!2Xt>Jm+7KVQQH{pu*r$k$Ju{E4zPws1YuNkrtgGP`qom|*+YQT> zSL_RV^ET=nm*zET9cPWFM=HA4b6l8x#PM0h37lo?Ju o25n((YZ&q51bi8&i=m#$n`=ejp4ThufH|DO)78&qol`;+0I$Z$oB#j- literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_star_empty.png b/src/main/res/drawable-xhdpi/ic_star_empty.png new file mode 100755 index 0000000000000000000000000000000000000000..90e58e684c61b42daa7c06277501d30ef20d975a GIT binary patch literal 1956 zcmbuAc_0&t1IKsKoYPQ_Fmh9j@SJU8A;+Bc9It7}eY8YT4b`JeLwD7#fel%ZvdJL`yfk3yARm^$lB^gO3xQK0x=yHpKz%GoP7@J+QeGD* z4wpGqC|&>%5rY<=6LBG?Lm+)sAT1Ocl)mJZ2B#o^Xu@-p7&ezkY4Mr8tw{NF>xZ=J z#aD!>hv^B`y@71;QotOJl`Js_QNT+{fkG}}sEvqFK zXn(U##(qZJv5#d!?oqO1kC%B6bLz5}cF`o9uiU)jC(DcO0muZ`ZC*Cn?v0t-?!BK2 z22>jA6^;{%gGOW7-vQo*2Q7+ zQ;Ia6TNAM}7-q5+XrYkxPeA8CD`Dgzcx$0v7-wQco*6u{krssh0)Y`aGmL1G57leU z=M&$R^%yrsBz|%X8juiHS9FF57;><4E3!Ba#Vaz3-r$fHHkGmuxN6)rwP7iu!M@6F zj^%ufvOzvtdn(KN$<|h{p1D*+$L>1I-G491{oLXr&7au@OZfP2BtBUessTe9sq-?fAt>kJJ()X@@eB=5PWAmajXrlUsAIjP7hOaxY7`2_ZhH4+4 zSd34-($kt88BM!l6BWUYMudZGzsYz1F=$|c{`qFblfQOLFcA#`il1NR-FK@DEQUCl!#+M1qVhLZ2_ zn_eFn{NW<;RxP3{MCjy^zxaN&lbeB#T>KrwHo~bV8vMWu>q+`{d&TC8Ld2#^|6fBq z%-@`IKWvMuAh>2TOrLM51?A~^y#Gg*^U+8SseGrRA} z^M{K7tl)DEwBR=AL@7`ix7=SYO~`K6fCpu9!*{!KX0;^)&!4C13Blk&L*|}+Wvp3K z1dyfDAx?N+p6j8WJBQ3sp``U+iQ`mCL_Q-Kc{7@Cp)NPQl;|#j6E>rBA4Qxn^Ye&X zbsg1?0kq&V3RO)n3-?oB+8|eTeXdWiKrpkG4}3$1o#5Z;M%BIbM>oeBkeD6JJo(nB zWyg!R8Xf5oA_|_DRI9qpUB)|+ZEwmRc{z05zH2;- z^pKM33`J=9PAk1IRZO9Ehe=af=Z@d8Gd%{KOz-21Y@_9^mG-_xWmQ`x19;njQZc?x zO7U-_S7;PXZx2!6+mg9(G!$^0U(el*t>sqhe?HYc=Km8bccV4aak#ZXkhf?9>=bme zzlww?JFR%-#{|N{F@PiEt7k>2b0Nb>utfHKcf9>jJ~3EoSiX+R(ZmI=y-?mWc+<(a z0%;4(+$fTXMBo_)wK|VHA07!c1sOkj6-$B)q_4YpCj-k>QV>`5=)dkdz>lqCIo}ut zEs3eY%)rIi`}BT9OhE2xe??MvM=tx98iyWI{X@!Xp|H z%fU7K-0s)2NndU6b2ST6Q%pLdJk8euZboWK_F#-^vPVqZ87n_Ua*ot%HU00h$7e_> zwZAeI+vZKG_6+EaV8hxgh4^8&f`nr*{pjGH?w3Y&HQDK__x2tmZXODy|7Ib&;(C#` zh(A$K)s*uc+-TOUAQkuYY);QpXuGBH$ND8|MWr{1n<~Z=^Z-NGTt|0^+-}uxMy9l)faJ@ zfYSV}j*VeP&XhmRBAMQQ{PE2(f+bZu`9yc6JmrWqk0vdK-O6$z{-0+4lcj{G-Q%s} UP}iba2lfRxJGwj2?Q!Y<0z;0Rwg3PC literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_star_filled.png b/src/main/res/drawable-xhdpi/ic_star_filled.png new file mode 100755 index 0000000000000000000000000000000000000000..b8e143fab2ab9b83e9e88eabbb1a11e2675b97f3 GIT binary patch literal 1509 zcmbu<_dnDR0LSsq9U&t#<80y1$~?YE&d8pJlD)-4M%US7UVQCMd^r&rS=l6!J^Lo- zkdc$K_c@BbeSiJ_`u*YkdjAK{L^Bgz23iO$000d7dfLd}Z}#tKsD3w<`REM*T&vaB z*02nA*nRQr5w|<%(DGSkwHMT;twMizi%kLrPgKYBN2@bmdJf2{I@5g1e~+ka6~)hp zX;k{?w^mym;LIqwe9*9C{(IR%MxrVc1HS!vWtva0Y@ z`F);+nt<1qQB)0ba9{qoQl#>@hS1Du~K6l$=W5T3T(0o3F z5IQPNDsaxU`}YXzx6y39b*E3ltVERc@HWuR;3Vysvcf)SdzOqB5f;@GOfomH4sDf8 zo_-|DHSZ!P9aMk4n@6lty1`mF^lE8Nq=0aD-bx?G$uSsB936?=4}n) z?@n7R>_CzLImnLsbY1bQ;D;$_1WYtEPzCj&fVrUFFg!FMyhu=D80|wlC8VOg8F`c^`6sbLlih^@$Is zdYP?0NOy(!Xl{DOyAf}q0_AJkDdGY~zwIlERoSv;F#wwO@%1x%k{4NNskh5ZD>B)d?ioc!S!B)WR5iyUXOsMX@ly{H`37gKWI`%~ zFBFfOcviHvB!W7@bnHF7un??yZ0OSS?UcM@-5Y8~fnV#0RT~do84-He-E*YS?iw?O z8J%b!Hq;6_86rfsf1Dqs5D3Tdz{>k#?Yik-%u&k}hj2^|OVE>SD~on9kEa&M!Ca_D zW~^P@ebtl}+qO>1wu^|OZhHJ2ScdaQr)_E7K0SIKJ2#M>gsK{A;cm*k>jNStjRbwT zyi=EF?XMnymc1D{h79$Wu?7036qlm3>b=<5UyOOf{jYO+ThduAIP-RQ1@JD@h5>_w z*n>|}MIxhq2azvVl&@aiU#lOFpn-|5Y~s=IWS$uLLy_=9g}}Ew*!{*!Udw+ zUt|)Wu|r15($!D$6(Xg*g}2KJ?IK%UMEc$)IlHH6OKRY8a{4P=qV~c5MEU_%yph_l zP^e@)w+9vRT}S_ww8q4<3WnJ)>*o`zXHsRJLlDjccVzP4#O8FUAX$Ew4|jYkvG{Xn z-Co6?y)%d!-B})*Fc1v{B??T0UncjSg{y95sE*}l1 zz@Lv#G}jc|HD-hsP9=>TAmtrA;JILW(*7w-3Kbdck=D!mt0mr&cwbJcAchE%s(`4$p-)c literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_tag_black.png b/src/main/res/drawable-xhdpi/ic_tag_black.png new file mode 100755 index 0000000000000000000000000000000000000000..37fa47fdaf3af054b7c5f745e049ae4cbf079b8a GIT binary patch literal 701 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdoh8U~2JnaSW-5dpm2lzKf&8(Q;cY zp@mm9Iwom&xF~Zib!zwe!!E)#x1;O7gQw~uF(J-Qm&btt85&c#92FlG?(0q3bo2O~ zIo86~=hW19n(s3#CXY4;2G$8G4!5Q{Fsg7)C{qn!@MKyN z%PHb8i6JwbiA1SCj~RY%*jJjtly!bb*zcm*wl?`IESsh+jPCyx>om*_Qab1Ka?>=gm=_n)4{ zVS3%{+04RczYcv)2ucrimC-(2aK~f^?!-K5Ob~kpvvh< zwZc(nk`?9~I^8?sY9GGG5a>HR8C{vSBD{PD#` zhAM|up-K%*9zqIly%sP`V)585q|u@_f1 OGI+ZBxvX literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_tag_blue.png b/src/main/res/drawable-xhdpi/ic_tag_blue.png new file mode 100755 index 0000000000000000000000000000000000000000..b5d987f9079ccf36be5e0de1df5038bd26c8863e GIT binary patch literal 844 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdoh8U>5dtaSW-5dwa`1LpV@|{lj;? zTT6F}Y<_jY?dZlu3lw6PG%s$RZPj^UR!27E-fD~4_dl^0DjKg;pENIj@i>pTFeblM?u_(|^ikMjf4;o|)H9RA>JFk#7)rB;-7W-n6dxUh1VI4vsOlZ(ighm zv_O1;$)}Am(s3I)qWKFqHSXQMA@cIfxt@86-FYWmP2*j3=2Y=LOg*!pXwL+;Cn~45 z7tM&T_}ybJ%x3!J?Hb3Yhq%-31+acmdG$&6Z`+#n_s=&Oo-Mm|XZmcm>+J;!tDRN8 z`?bhtNBw0klRd(J&2WF2)SoQ(iVoY4%OAc7w|*r4?pVi>dCYeobjGtXKJNWz)zrYk q!g|0$+QT7$p=Wa=`5X?We=ONX<_VVaTRDNbgu&C*&t;ucLK6TZOMDvu literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_upload.png b/src/main/res/drawable-xhdpi/ic_upload.png new file mode 100755 index 0000000000000000000000000000000000000000..0680633665043d30b88f56b003ed6251762788de GIT binary patch literal 434 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdoh8V660XaSW-5dwcU>UQ>XGYoO$h zn`Hs^d-karWM;)B9^SkBWZEk0LyuN;>Y4MaY*=vXhnj*+V*`^D5Jjw5CV zCV#jk7|q~+EZbFweGkWa-i;z3lJgF% r#c>^hIQM8hg(c!d`xtuw{b60%)f#x>tYa=PE*Lyr{an^LB{Ts5qbH=T literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_action_content_new.png b/src/main/res/drawable-xxhdpi/ic_action_content_new.png new file mode 100755 index 0000000000000000000000000000000000000000..b8e9884a8ca50e01b653867e25de172778dd5ab7 GIT binary patch literal 603 zcmeAS@N?(olHy`uVBq!ia0vp^6(G#P1|%(0%q}r7Fe!PuIEGZjy}e^tbSOZ8C9veb z^X(lT90wYwC0w<6>ixFl|HPEpp9+hYa>~3cIdqq!PoAlXvro9=_~nGA$1IONn#_1$ z(O6-_t7hS+Am`sqQ>lH64%&ZzJ@xqGx}$&Vj@RE*w5aRhRI~6__|Nmpc=^GbpMP_> z861pc%DWi3wLgJHxTcnA-ofkTKk|W2J}2~n=iI8VlS)4yZ#(hw8&6+Hjd*iH(`J@E zDXCZE9;m*)yS8-pRVaBG%#~bM%+>of<6EDuVqH^7% S3OQh+VeoYIb6Mw<&;$Thh7Xtk literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_browse.png b/src/main/res/drawable-xxhdpi/ic_browse.png new file mode 100755 index 0000000000000000000000000000000000000000..c0b93d9feedcef31f911dd11ad266bc2c272dd1b GIT binary patch literal 2460 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 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_collection_black.png b/src/main/res/drawable-xxhdpi/ic_collection_black.png new file mode 100755 index 0000000000000000000000000000000000000000..66347ae4604fd8cd4f844c4f3db95bf930ba6a54 GIT binary patch literal 980 zcmeAS@N?(olHy`uVBq!ia0vp^6(G#P1|%(0%q}r7FwgdMaSW-5dpqY~p@<`o>sx6R zMI+}LG3tD2$Uvm%2*!))yfdu(NR9z5eR$>(Qh-m#PE?Crw`8S>USeEwEIEopD< z4@n{4>tfy?`&t!q`*+Xv_Y)`~;rsk|(oeSc`IWK$N$mLha?Q&9lfOUy@$>o7>bC90tb2BT_ucV3nRDO$ zm>Qi`6Rjyy_T?;RUOj*F=k$+s&P{6`+;&T6wy^$h zw3p#e{Yu6?IV{->3=#*9a{1()*WLAu&#+T{-y5!m^H%=r6`VxTBw z-K;&yUdTH5_CiyJbN_864sg~RSVSN3_~-FPvHYCQv4a*BX?{ygCwlfDIVU20QoeKU z%C{G!WO|zUdCyEdI(^m`<<|FW_J+i-PcF0A@iT-`;=pT>i0#2A?`}#7mbp{2%Hkx` z2P@W3<&SivfEG->99i=8dlS!7kkvC5@06LExAV%9^CwNWz6z=hWqM}+XZbVLZ7YNf z&cEo`IJ;{d%bDP^WdcXVfKrx`hGoGQr#5V7s=RbpM6EW^^ugMqd7&#m-|pSR^x&>i zBaQjqEWVp6< za|g=?R<*qz46koRx-i{fnjELnQ2JI!i7}_qQ(x%7t}?NS4BHMY5$B|hu|LJRF0@pB UNjkL|m|Gb}a5V41ofhVDm{m9da2@KuV3VQu4%n^4O3oBJ54m{%P zIIVn;L9E;%Mvjwh!w%+0pF9j2j>-y5cW+|UdDpPRMu<4A1-tod{%^9hi~1jx*LHSx z!KvvF?{B=AneRNeyHH~PJ>7Y0>h{Sl|6l&|rP;yd^RJ(O_wUa?F&3YM?pia(FBcad zU$1{(cU_n5l7xqPjQ%HOubDqz`Y^9{z5;*un-_79%q^T&uj56DET|9j7B1hj8)F%-&r)ArcyV4%Mb=m6EEb`lK=Cd~N)^&Y5T>j%E1KWp(dWJCvMRPNrg>{j?Wm56;o{$A*R|Zd4KbLh*2~7Zi8^Hen literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_delete.png b/src/main/res/drawable-xxhdpi/ic_delete.png new file mode 100755 index 0000000000000000000000000000000000000000..4ee26a5c07636ff9a64c749fa06825723f28447e GIT binary patch literal 851 zcmeAS@N?(olHy`uVBq!ia0vp^6(G#P1|%(0%q}r7FiUy5IEGZjy`668#hfU?x>;pG zm}P?khvrcSp|xvmo7p#fQA>KxXw$s4)Wo$Z)j`0-F=MBTl}1 zcJ|GiS&I(#n>HS*l6@@2VYZ)BpeLc2>DgxuiG$Br&O8q^XgbR|&jj8`_HC*&ajZN z>o}fXKBM;m!%q34H7d~@d{eU3Ewc}L-cj0}eotIJ!$d0ewBc?3ZBIHLeA;f_blNy4 zU<1d*Cf5S(_P~P5Hjy<(OA@Bud0rSP9#bcm?y%vfZ9@Xfb4P{k-wV&h_lEUeU6#O- zy-+dY@wBhu%3NYg*U24IwqkhnO0&Wx+o94W`%Z;R_nm8UZU?0TxvuR9+;F0|srg{w z297EJ7r7R^=6fu=hH+^^uk`zbiPG;MBov6~Fg=ykX?iRx!`;{9I!{EWY4L*ypy;P1 z34$`L$D0->OjKv>exP9?Cd1loqZz^CesJRhjff|n2bCz|c%|9Q3w?s2*JwrqR!+G}6sz2=>H{`v98ia%d|XC?Oa!ZnT5gA)z4*}Q$iB}i&lJZ literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_down.png b/src/main/res/drawable-xxhdpi/ic_down.png new file mode 100755 index 0000000000000000000000000000000000000000..d925ffd34b5c1da0657a26095e2fb8822c846019 GIT binary patch literal 833 zcmeAS@N?(olHy`uVBq!ia0vp^6(G#P1|%(0%q}r7FmrgiIEGZjy}jw^tyCy;?4z4U zPPOJ-7yWm+{IZK*)UxUBezUZ7?^ZhWZE)X3Uq&d~q7Y0y#PMF%(Ee3P^JX3U!Fub-y>=sEuQWrj)B zob%7m&oA6#)-TAn+8XsYbMu5>#lC8j^Iw+RUwZq#z+z8dn+(?v z-gi&s9;-$zbAA-O}}HSmt!^waUViX}g75*tnnUFx!SUG{L= z+*YP@u5vtV=G{jheK$OPV}HfF%gOspZ1k=at>$^yU_5dD<;*WubKT^NCK#XNQ7m-U z+rf0&;iS{Kig_D$ENr|TVI*sA^Wne3cSdo>Kl&$X4<2QZZ;rY5{lN2=7vAsu+I?z5 z_(k9FW0I%W?BmV)mCnf}zV(6fx^=VT=Vr%_n$wjiTrf2Heh};|J1-bKfmvmy`NJ5F0`?%e=l0#r?}-e phmY{ur-Bg)70e$h^aeY%eu;oqhp_80BVg8G@O1TaS?83{1OV-)bK?L2 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_edit.png b/src/main/res/drawable-xxhdpi/ic_edit.png new file mode 100755 index 0000000000000000000000000000000000000000..b1496619b1f5dd736db6476d69e0b95cfc45c543 GIT binary patch literal 1047 zcmeAS@N?(olHy`uVBq!ia0vp^6(G#P1|%(0%q}r7FyHfZaSW-5dwbhHPdHHE_{STq zjmPYmdi@i2cV@&mFtU1hbaY8OwC`YMm5piW;$jgJDPR!)r|dPMhL!6!>mlEYX+k20 zs=lB7eA~eIcdv{2?ma(i%l717_k7(Uf1PtnOu2ipJA?Ip4hFUxl}zVuCpR&c&1?Al zZAL=FT=fTgY}F(Zo^x8{_jA%j+Cr$pMlSwtj+wsG&&40!tNhs$xBj~S^wUq*p9ual z-Kar1$==Rx-?qDX_uDT;i=VPz8@BrQtci^481xR>S_hdL9Z)?WxZ~AC#(T^<2SR&! z3K$ER)a5u0wA3Pkk`m0*f|Im;{ujcW5kUM%Y;U*umg-d1C`Mq}Y&+m`*`Nr_1)FNJgcF3bnDgK%~XVZ-Bx6PMdetF&J#B7E; zz9lEiA{IAtmz}mRQf&9VwfbhxHw&;@?wC&+QrlXG{y+Vm6=1Ys^0Iw^=4G=?;g!;lB7&9CAPA?_gOcST4T(7H{aqtq=Gl?(Cl# z%h>$;J4%0$%@(he$m0+!={`(c{b}T z6ADsyENeJ8_3&3NONRjhj4lqvizOY`jalfujoK6rLG6hs~kW#!4Mi!nX$;9B!)Hj6)>ez=#N z{;IJ0MhfeL$x^qn&nymzEQ@PAXdC*SgXi7OiQm}%SpHGG5xcgYBkXYQiceGKHS903 znp-{j%3YYn0VgJ>%PWputRme$F;_TYj@m@U}G~+(oYnXxWf_SyIxB*f75~9 zBcaSX5&BCHv4qsPa9{V?sleF4ni#3WdSlV7gtH9Xhc-oUZE!U`IEzuNC3T}nLU&e^ zDYFj8=_JhqVpmzR*&+m{9Sv&GUB!8gXM=)H_fm%F5aBhF362qB-i+H;s6`kbXxgAN kjVUM4cS9Nt4F1Q~^Cf@!o%E&qfEk{_)78&qol`;+0FhJCtN;K2 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_favorites_black.png b/src/main/res/drawable-xxhdpi/ic_favorites_black.png new file mode 100755 index 0000000000000000000000000000000000000000..6416df4480168023c9e26572d69b393a6e6d5d00 GIT binary patch literal 1730 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 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_item_blue.png b/src/main/res/drawable-xxhdpi/ic_item_blue.png new file mode 100755 index 0000000000000000000000000000000000000000..f91d3af8fbf8e577be8478b3e754ffdade5f192b GIT binary patch literal 1138 zcmeAS@N?(olHy`uVBq!ia0vp^6(G#P1|%(0%q}r7uvmJ!IEGZjy`8(?Pb5&JJw2jn zMaCN607q>@!($nbrGIe!VqogL$i88-Ss-&(YVuR&jO@fjCDDy02c4{ix&P_7O?y3; zrKjo5yxrF2-{u_NuA82A?&rSWx2>&bZ%93O+O+Y~*Akv^g&RMW91NP~3K!VPak_0N zW;%CV`5;G`-+`YMDiR0Jaazdtvnb!NWPJX3Vgie``hyx9p`L{2EOYwZn*`s?Y54rn zgILY{?FU-bAD#%y{_7W+b6@Y&r=T}UHpS}v;m`bRPSo@s^IvataI*h(|Nd>;?z>1$ zmsWe^x5?_D@%qCzj(y+CANXc!+tYV(=ccBVHZ7mO_FP+l4xi^b>yxio{B_su>z|Tu zqP!zUv&*3Lz}6U*$i)xj1%IqmW0}78;^u{yE1yShayw|h_|8T(mgBph75#Gk-IIQ@ zJ+5Hu1RHO+59gv=!%uSj%hvy(A$hRctU7;^;Gg-sE&V46)*N`dOZ+6qyHAy8np0T5 z|2U8vYt+Q*AEM*!w!vY;({@D;F|KV|k_S1PR41)|e!Fw|nW%NEW@cUbQJT0o-t3F> z_S^G%9>}@qa=e?C!lEs7$G@stwmE_2dzA9le}4nsAKW_Wj-@20+Cj@}R~w(J95j8b zc2H%Lr0Rc5rGb*mI=e)C^r3yHR0xuf#JM6VwAsm+4kzGo7` zSO0%;=R3xwJumQP4UJm%f$N}VojT(+7ebjYpGR^;5d~f p*|c_+@VDLsMUB;|1Ecsqn_JYaj*mO9`~#LH44$rjF6*2UngARq99sYY literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_launcher.png b/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100755 index 0000000000000000000000000000000000000000..361d17f027ab49073006ea7638c7c4a60376503e GIT binary patch literal 4911 zcmV+~6VU95P)y zojdpb|F{4B??3;&;{twg2^f%o%fPk#QZ51D0C5NmNq_@nNPPTa4iIO+kOVkDhQ!A& z<^XX93`u|kWJrAcVh#{zz>owuK!(J}FXjMo1`J7n17t{i{9?-hibNt*w|H_%vy(ss zZyj;DT=diQ!&HE}pb@Z@VQg^N^w#nU6P5t##*G)#*8-IyE zr(vUp;@Jn!3^Jic8p*FsSOTQqc07;cxd(MrQCL{`8hz14oh4jz*M)`w5*u>r)TtZs z_ZPdsiB``Ik5ToQcRfNQR<;MN(r=53ias#|B0~U)4QOa+xC2{P-USWOO~2B3Yp8zn z#FzFpkwT(AsoU-Lgu`L*`~7zf95`@|QHT;&usDu(S)Pqu@C5Py-n3U&Y6~EU5P4f$ zTLHiLKN~)L_(w*3qJ#jV*%o>7bYldlnuaa>T{AonZ1h0m8J8}#rEQ1S4K9el!qFXi zni|By-V-NI>|42VlJ(W<|L;<21S4Ul4-Q6CKXBFO@hAofQn_s^+143c$ zS8J7*TDj|r+F{`}O@I!vTG2C~qZJA@H8pt-A3pqWadGiqtE#Gmv17-cnMG@@C{cji zWP*_3^*BwRh+`_L!;H0itTPLPRsMU@+*xaLdNgqeqXY;UtmRoo;}-OdYIP zu_9;2j2YkJO$Q@Dj{EprHifUn)bQyrk9QyHEKJ}x;Q{;y0Z3jDL|5VPh5ydXXt*oYHMqQ z>({UU>6|%pwyj;eHd0bjqRSgk6d*U=Z0Pv$NIRQKsa=H&|4SQtXVU3(xgdWaDdd*YyIa0(4wC-E8Zv;L{fm> zc;k(+3l}cjio;h2p1NjYx-F0h(CXEzFPk!D$`%fg^?<0|K@A|fUJj8Ipz`wan@USd zzv2L?N!p>Gs{teqZI~T#Mgpt>^1S-$tCvlkI`wmIf!I?S44-Y#Hw+?8H+dH{+;s7}ihD5qF z@xlN+Hrx;HIO_&u4Q(8tM5OJoYc471fM>34hQbWx3=Ac|hy&y>81!~0-y4BvMm59G z0_A6)@tkTApkv35wZH!Q>)Z#@s{lKf7ovVo3rrpmQ0#`)Hy*I8NSBrZoLxxd9oHgnE%hW?rme#TrPYP-Ssg9+ zPyW%jM;H^wd@v|Cw%?v+^vOBAJDw?O*bI%F1Vp@5T3fy2VVENPIIDtcN9QIAWBq#a)l|? z;}5yvscnU@vysi75d(6=ums~hy|d0;ZDXYGQh;zZ#<*vnebzqD!j!&kdWSP30+hBv zFi7AU&4Ir~pNE#H;6{quKD> zu6*cV;P+6F;J%@Lm~rvB+nAWqcWEHeQ2=Ry$P59}ed5*gKc5$#+;$$E3b2XyjOibQ zCr0?dBlCOA>O1+-Vj$6R0L6pIECI%=#~1Z`&V${Jnl~IV)oC7-*MWYI56;i*?)RA4 zcc~BLNPuENWTpU>G>-dIK-unmSbsRnR4BAomV>@J`8^|hcK3VC?t5AQB!$Rq0V*l< z{hDm}3;8_^_P?URa^~fy;r2n@lf$g&d)fd*iavSrr0N&g3?YBl-0AnQdd_uI{{YM# z=?6iQ9A-wpV)=V%1&|sqLe0Kp<9?4X-bXF9|!{jh}NILAivwn{m{fal!OiO^|5Sb<9cWZK>Y*#L@ z*!Po?;vd4K_$iX4c(eKyuO4Y2@y$2ibWR{q%Qu{SBC~|Nr#Taz+*$y>Ae(rPJSpC+ zeyP^WfuJHt`;LQoNQnn^{Kc1EnoMu^`eaJ<_Cjt!*|F9;%B2B$FtcF=?E=Iqns+LW>rywH&i!oSu2`&a?2@eM- zj;)CWk*KVS13B=@-W(==kAT^*kK$S`nzplshXbTCSVXi@wQ+&Bxg+2JD%Meh((a{|FzJsvXIKr>h-j zG7>pJaWwP7ws!d2cR6ahv*dH~d*+Pr!-NZzH$f0@oJ@G=Uh?f-_z)HYfz`VNSn(~J zVT+nGbx<2TbOk1db-sjH@klm2awnQwpctyzjtgFARREDRkZD#_Bw8dRo12p%)0(^YL%`_?gwt?qv zg1{M!0C;FzC{)7+U9?}N+mNjr;acr2@; z6&`sUC@qJuNB`r2O-JydpxS_M!RKO9{H+Q*#iz36i4}=XJAmHU>xO091>A^*%`pjg zivJzGAtwgDc7P|XEzr&e7u>hO3+*h{A*1An7IykQv382L3p`SQD3DlQUY;J)_*Ptg zOny&YD;wKGN%6FE7$wE0dYnfMX6e%Av;k<=Cjxw0tMrNyYlky>L(Zd^9CkBK+wBgI zJdl_c0Ik_A!18b0%>14y0|U{dc)8oI_Dv^crD7TBG~V$5RoA=WzK;cFuNk-;r|P9K zlHyYl3^h9(E0E}DfN0y5+us$SzKxCFLrL*VuJgf=yzYLFo#Bzs!W<3IgH-}-InE}p znUdlkA9)(a_wW9k%kJ=`4M4B%6yU{bHt>*;V8i9!CT8(hfJgvO_Qp2)z}R;M;L#Vq(0w7|@CE zI2NEUj=SI?@_STPa_WUbYx%2sg;>|FSR6&!;b>C) zKwK2)ba)&I(68|c`?u<^#IS@dFZw-yxT*zi=-cV{IN9=u0+j9#WP2yN>}vu3zK31B z=Z+z5_;$d~HzKD4JdUzJ@BZM1`CkaY;H{ETV2bkd(K8CftC@-RE7c@rzM9E>(-p8c)(`=k(Eef(* znMK#P!Hxa2Z2~F!K*x_CZ(p-!jr{_Nk8Z}K_!>4z@pc14YkTzqL`$0~rPX!-mG5@J zbKiKFC5LHMh~3KR0SNmRUD|9rfOcVW*u9t>#>(%p8x(rls}&&W(q>x$DShjR@0E+u|f7`RtL_it`vds|oXI#rwj%&L3VT}aT0FurkZ3)o4Eds3H&jGRq zAKYRljs=p3AUHr)X~D+zDghKLe#HT@v7)VNdMtpHf}b29tF&O_dgTDci=11xH9JrX%U*6PJJwG%*Eo`uQxNaO(7M#a`P zR0}|{mNs*MtksKcYF7g&Rv@vqw$?rmQ@IahEsOC0QVJw;fUMPtt!h^SC|)3u89=L6 zt-AcS+iv?Dr-NRa5~8G4GsCzAvX;kK0I3BMj~+c5T)TGdxYE+nFXzph7ot;7D^-;) zuMBS{l$4ZsSFT)n$@J;dKf}*^aDeoP+`6)IfV2b>4;?zxO53|EU%q_%v}x0V6%`e_ z-m<0}ASBd{XU2pH6MEuukk2ryZ2$s9S_g9z0l)o0~fp0V1>KQ4PU^!5)f*i)y=N9!h3E#E9HT1d5e%xd#3gF^7*0(Je9u7bfJS)ZDF z?zv||eSQ5gY=a+9N@vuyAyXnl7+vE*i1G-cB}_8Z~OvllV7eWo1QeD1zjoGnRm)5NYYSQ_Ca^9+Y1M!HI&=-rg?k-Mbe) z{q)miPe1+iE4_O4@}mW6!jEY9BHdXIP!wc5@jZQz)2C0L0$iMyJ$LTh3B|?5v-|e# z+yDIY&xam8dc<8SOb|I03@!ai7CdwxY7aWhP*+z6IPt07ym|9;IBkC)b=QizBY0@| z((OP;>(4ZP90X~`kRd~E#NYelnV*r7;pvJKol}k9 zPwiGdQc)3V^3u{~v&yKog+_pII2>weX=!R`XsF${Z{Ox+%a*-~y7HmU0{Daci55I* z`+eQRml1$OD}--JhitM$XbSVt6neZ~FM;L8zYDIu`syMCNa*NLo*AGsrzQu*&(SO= zd~G^hh`Mm0d_!BeZas;XrV;-R)8D9@5bBD29{L8;JIYB3>fX14B}r74WQ9nB1W^{A zJ?Ll8o;|Yzfq)yWiwhgfHJqq)O}q}F3FqYGL^3lo!)S>J81i>$KO6cf!lTRg5u2Cj z5GK7|gou118q(+_s65zUH$ud2!$T95VArY;o(}v%(-1%>!9v3oo#gWvf`?>c6d5RDv`I|WAsT$On zpFemng;##%rsoOq7dQX=%Qx@o%h1$ED-U*F*!*)Bn|=5DS1t_?TvpWh73@49G9Q(- z&+o(kDxX{5x(Z*#2>x-N^kAj()-SOp2U-3LHr|wftbHr|%bQNG^3U#JN?&sJtvh)B s(QYe21q(r~`NA=J21nJ0AnQMy#-xPGdEXZ21Jfsir>mdKI;Vst0Lu9?8UO$Q literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_plus.png b/src/main/res/drawable-xxhdpi/ic_plus.png new file mode 100755 index 0000000000000000000000000000000000000000..2c8c167fc99ebceb82d1bae4d34deaf8570b5c41 GIT binary patch literal 874 zcmeAS@N?(olHy`uVBq!ia0vp^6(G#P1|%(0%q}r7FdKThIEGZjy`8P-H#bqh^=GbY zdB9ONEkjXWx#N6VyBT>BIYmu7xMGBx4kUVR4?HR6QP3YKVOKo)=X;*X&GXKjDL+3) z;a>G^!TQC*H{AC39hYUS_#wz3ap179#6KGzwhtdz+U)t88EtGDAD%y)z`*aH@Z+-q zsnUN8g)C(FmT!DndTE>Z?QM6}zG~$fAA7)1Bj$Need$kShX0RMhrfORE zLy0|VO$R0JoW1(hPNL`FsimLnBRo$Bb{~{jb4QX*JJT}DP@;LN_2C3nU3Xo!jd&>c4(FcipX*W45QcKyX)$chl z`@bju@VjP}$gsZnVf55$>6PD(0|OzCjeDd&l_TSb4y@F%C^_iAZbU>;)dboFyt=akR{02bnayZ`_I literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_scan.png b/src/main/res/drawable-xxhdpi/ic_scan.png new file mode 100755 index 0000000000000000000000000000000000000000..ec827ddfe42c997b3cc6996f483915108fbd14f1 GIT binary patch literal 764 zcmeAS@N?(olHy`uVBq!ia0vp^6(G#P1|%(0%q}r7Fdg=EaSW-5dppO_kJ(XxrB+aJ z&jfjgW7;PkuL_D`00m-0pCY zt4+A!_~wD3Zm&J~de=SPZR;O@wTSy}USQYvy{6~-=}kJzi{HO5dd3@RUVVCA(|xzD z<((?;oj-hLJ*azb&Iy~pC+0QT=jXh;UvOOU$HS%vV(EUjXH^vVAAEhJcE7x2)A@VV zf4cJupXI!p`CR0W^V*_}G?%k>ixXJSpF8*U`K~|KzITkBzx-Kgc7rYbneN3Nz4Ww> zTMt&p{GGJ*q|O4)ZeYpZL6%nU0k19FT%Ut5~nS)JCgtNHH1 zsgtK>xJ@_Kw%iTm%&dHUY16ugKfH-INfE^*Z=v_j2!P6Ag zSr?z4@j3_Mq}|)KMYr%gPhj~5>m5a#gwDPwN>fb-ax9GVV{c8{k>0k#`i5+bW#nQ> z-8uu+!t-9|s^2{jv#&H^$*(*AU+IRtleWR$=w|LsKLwi`njR)ENMJq8dEoKQfueRi a&_8M^HeLA6IcH$1X7F_Nb6Mw<&;$UZkbT4e literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_settings.png b/src/main/res/drawable-xxhdpi/ic_settings.png new file mode 100755 index 0000000000000000000000000000000000000000..39205bd1b876b3d79fc129ca6e8883a589e5dcb3 GIT binary patch literal 1706 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 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_tag_black.png b/src/main/res/drawable-xxhdpi/ic_tag_black.png new file mode 100755 index 0000000000000000000000000000000000000000..c116f92e1daa53ba0a45cc809dae3311d453e7b9 GIT binary patch literal 1121 zcmeAS@N?(olHy`uVBq!ia0vp^6(G#P1|%(0%q}r7uxNO?IEGZjy`5vdhBr{8?fq9Z zq0UKeiv>EvZKSvYJzPwzLRNHSOtA{=bZPYSRONC}6k=_4=V)?M7GmvmQgS@<`2NYY zDbL=_w5@*j?&oLUO|xe1y*BUv|8rL9=k_dH9p2*B5{lW+JiU03 z!L0m1jGXMiaN{y%_c+wa2g-!33Dr6I;(yImuZzEfE7yx&7nv?WSu+nj=lTa zrxq<$d%O5r`{u&fUr!rPXfy{5>0AoH1S_$}?Dr)GUwEWFDm z_t+5~26d_BTVlI*cUg+6?#sNZc69fbPnAZDcg`L!_F8XURJSPiw&?y{z87_+58VC3 zJ>61X)9lI=%hg{?|Igih_Ktz{0p8wO206XkY=d>yNqmleU8W^hGGA>iQ^Vz&-}bL( zgvG4V{93o{@cw5rSH+4lZ2!DEujIqWWlAxTtD+8xu5_+kmKDx=;O!qTR++pT+~z`j z`xq?xpVqj#*XaDLDtM!Oz%lHMVy?V)!ap_B2I;mQQ3G|W9>(HTn-uRDmKlgkM;(w= zPS6vVVt&?C@4X}WRYKlP&jxF;19wWMbCl~e?99C;QSj#2cil^J^JMk%9>$c&aQ3e| zV%ycH@zY0)f4=L-qwF7kWZYT2L~v=1>t}`vp?wuWnhlk$&T~R9bp$c=zf{$Em|-bn z`dMh&H2Iw$q?Sf^6&$Wu$-O@CmB+I4;tS7S-WO`yDsVeBu0^c44x0N9ZhqeI x^9%#qhMmmMKD|g_m~EY)*UvXlTr>7R{L!M5DoZA4yaW~<44$rjF6*2UngB<_2AKc= literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/ic_tag_blue.png b/src/main/res/drawable-xxhdpi/ic_tag_blue.png new file mode 100755 index 0000000000000000000000000000000000000000..57d02497ddf408cd1ee69f23faeba59a344e222e GIT binary patch literal 1308 zcmeAS@N?(olHy`uVBq!ia0vp^6(G#P1|%(0%q}r7usrs3aSW-5dppNEBREjz*m=)O zS*9y5Tx8$o$S$~LpSbK1M}fk^VDb1Z%j_Iv?jG@E2^1DBS<~Fn+||s%XZ6D&Fi>1o zGl`wG)l75kttUVDJNV{J`0e9wx6fhOoX_^pf7ktdxA*zJEsxl5G4ihMdnCfx{GXF! zM#Di_hUv#ygbj8uKln5wfr+i0Ax2Kv=fDHLhSSMSoDz2!3o3mK7@Msb^!ho~5(?N4 zJT*MX!t;({hm9H)HP7+-@JV`E@u%q2{f1fFf9#9dSFH49zjGGr#ox}q_FnahUtfAw zx982(V8L^m-+~-jjvK9X6<2$hCbbXk0fh-#oYM)&G;Vp|Vx3FB5)s zpZ;9NxWzHog&nOMQM zQ{O9>-TLXJc{!VP*;T=t^6@2%%X0Zx-Zc8eC)P~XSt##ydCAhBDW(gr)k&S%^F3hI zQ(ul7i=S?qYN_h$Zon=exM%K3m?;hLRER{O%$%>_}s#Z;7SfKHUfk_TDo^FVlPbtU2fIM4eeG|3gEvI+*EOX5qI>KkSmfY-M9N zS!|z`_VmTWclmawk6oM7z#O6d`|<2cxy#qC)?v>(dFkY@=!S((<%$m&#Ps%9UAz3# z_@i5bUd9G{JB8oL*{7I!#8+`MvTxC^-ni+g%9+}i^EOOb@xmpyw6Ne_d&5)Z*KP+C z*G&2PQ8H5A+@k2(`Kn39=KCZr?04c3+@;OfzTw09)N|@Vi|$9ct-f3|ZS}=lmPbz9 z+by)AeEk|-@#m*c3q|dnAirCOch#o6doiag!niwnkL2Ltwb3v9?gts|R~c&9w`(Y7U}yZ^3;;+@`RRJ};zR6Z}$x7Rb))(Rik zG-sDy_Tyi@ExP{&3RtEU3*SBBGJQR}`Q!7=Ytr~mzJJR#LQ$}d*?T8=)Szv-XU(!P+leS4I%x1L|8n|rtEPbc!X9D%}E!pSXU_U>n=Fdb`mEtL=V0!raVnV}VVUGBIHfEW54nL|)Bp!TVIdpyTL56m9fqinkY!)_+AHHT# zr1iP#hh24ZzrXzIA9rp2ua_IubJ?r=86P|bVad3R59b6jKb(88uSS~JZk{6Vz1bjq z=lSHkP|C9@Qu*L^C*z65gSk64o(Md^UZ^}VxxsZu;R(S5 zNq01!d^=EnsMj%%we( z>_FO%wG-Yp`WD_+h-RMsc$>pEwtK(23s=QYDgPI}c`;7cgxz Nc)I$ztaD0e0sy|4Q*;0T literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_action_content_new.png b/src/main/res/drawable-xxxhdpi/ic_action_content_new.png new file mode 100755 index 0000000000000000000000000000000000000000..d4cca960ee21ac55e67e5531ccb53ea6a1c6fc61 GIT binary patch literal 1077 zcmeAS@N?(olHy`uVBq!ia0vp^3qY8I4M=vMPuFE&VE*Ij;uum9_x849UUVQ2%fTQ2 z+Wl&pB$&6_F)!Uxr_>nKEPXhBe+`EWSB>nuZL<#OGj=@)acEfnXBu|^^KvF8o2T1j z8l)^l1s-Vbf68>_z{&;&|DU?z3)uWvSt?HD?_@A7&{8-MTz8svMPo1{9k_Ox zRmPUpqG@M??hJ-#fANAZA`dv8F>Om=%CU_-a51<+c?S25Ce96COBmh#8T%6SXrjsf z9n(J%kJI1(Z1MlSKbg_){snJo)*nLmfA4?8wD10oI}ezq&Eh{(#3=7jm)gqxqwC)1 z`=yNY&FeE4b7NKYZ+iHjl0mdKI;Vst0NDMv(EtDd literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_browse.png b/src/main/res/drawable-xxxhdpi/ic_browse.png new file mode 100755 index 0000000000000000000000000000000000000000..3a419f70c1c78da62ca1b6d3a951444279a87d3e GIT binary patch literal 3853 zcmeH~*He>?*2SL?LXjd!@4YC3bPYv97a<`aF0I-Z!Nc}7kV~LD zs-E&{+ZTOGQHJ`rN59#>hZkeJ)YTppK4z)0fn$dfTvC{N_0cSH460)`0;VF3wa2q_ z`*lzrKfJcv!-2fW7`Kg6L|}F3=4t<$W`W95U!y0l5Es&_@V1h#5gd}p4$~_v2S8TO z3(`4Y%Kr_vEsU=GVLxHa|Ie=pJ8Ns}J1BKXfz0ghu7eY9hvI-SqnYFFeQSqnoj=3J zPrwD5+aEa!_S=Oz&)uF(9bRB-(}Mr(e;KOl=eMZ`3xc$qDUx~vu(E8qHxD-3YQV7C z(XlZfYIEfN8YIz9eMU~WjYuS>Pft(V(%`Nc3}LSXu7rr4tYc2z?CtHD*OJM*oX{3C z`ukPDK%QpkZrPnTFN)Buq@-ZkY&*4(=v;_3AZLIb2{^mJL zlZ`>~YjGg==A$E*vU|u<_=Z{OfjcN!4m5A^eB6}RV*;E^%JTYmJJGbkTrY4?tm^G> z!Ut%HK_juo8$=t%qnE1bUxDV5GW`?gIvzji&jO2*GF{nu{-Tn(>E2f8_qg^P=Wz ze^)Td>bG{HWlr{7#tH-@5wxODxJHAdcAXb=x@sK*Sf97}6UM;8>M>kfR~O?U`}x(& z<&cw*W|sI7Mcw>L#^zu_EesZm-G3$O;x50o!6}^AABNtzeal_VxAg9IBQpoxGCh_iW(ESof_T{9g4M^Q>*ymz$yxqn)p5B6-HU7BbNWj zW7I|jBwJ|*sG(>WUONR=LgEL-PUQ52YMHoTIsa@~sRoSJnyLLlh*|Ps4{=OUALHlu zoY`lOq|PdpBwHYJy|k0F(u%ZuiUIG1GjjI$63v{J`&hXt5|9x3$~{ggctS8T&>B5 z;DtQn{Y2S3E|=ec07>vXjbP@ZQ&#a#Jg;&y_ev(e3NH^QW?1!T0cq-@|k9 zQm7)(vsyzUkwRP$FK(AZ;Xv{5r5q`3X%<(Q&NI78xl-OfD@LBb%f-bm7d>GflnU0^G`1IT%3Rr=>jN_sQ6cQ-PkTdp8vJfH zu%qM@?22D99*6@=^Oejd-Orswh{vrmYiq{J7e!~zU(B7F+kEVTPTada`wpS+$j#5? zG181)8JVNtgfhQv9nYUEX!}E@WxY)}qRUQPgkhtYHPP-P+H?^}p(Ps?Az@z%+;$8I zx|!S*qb^2WOKY`iC>d797O(+PynLC#AF?&CCZ|4CmTuQbO>y9V%*+ zWCHNcR9l@KdbZH;6sP$WWf<97xLuYlXJ-RVNn#c?N+PvC_+!gxo|G3xdp%sLI|%ic zMFxux;-!**Fr>eJt~Kl6)sY7J)PETLBiBSz3N0S9`KK@S`>Y;-G=j}=@bF_{`pT#D}CVqd^+K-kkDLZ5?uY+>zV{`+rI~7x7ky-sLTwR z-=^%x;uDBpJI-~Sf}Rf!bGmmLzfahLe-fr?0qCHwvd}TYtY5k&Hc6NY-DIi@zL713X@YOeJTCd(z_7R7iHg2 zIpxfq&DRkR5R@xfHxKHv(;~R#+duN+rd~=nSbmRh0X|_5K~2%0Yd__t5s0%wC*x*c zTKOPyQ&YqMsR~AIntE=ahi)lg1DMZHlgP~|NPHTlLo_rA8~tTy8io+t&MN=Us*y&K zL%6-n2ljvf3rjKN;)(q)`s7EE1FEQNEp8W*D<##6E&|2BkuE8$3cXhg+bWtTubNR7 zmTl0KsIMABy!xY84IjB20z=M`tO{n=IOE&TK;7z5GA~0Z&>W5%9FUU&z4-wR%=jx` zK}3WOMoW4f+g|~_5Q~y+B|64zFBEh4X<&%^bGPVy5sLz_-t`H`PcOJKS%tL@0w)hX zFr&!YRo-9H(Jg{?Zl-q+q+>9h594|ow|URXFTbm&vM0S$sf89u_;ALZ?#(ex1naWe z{YWM2aPcV4oJM<3%JH|}e-`Ql{UgVKKja%eykhS=JQb%`e<5KggGdX^vLC$6>aL{M zQU16~%FoZ=6A}_qPT$MbZ&=iV=AhHb`m}Y+dq{>Sx`m?N>X&sK@7z&ouBceI{=Vz7 zxLm2@T+!R{!yfPMX2lnxBJ}VxyyqU%kFl9Q*>q^cIa}V>`??2+=_}cb9|8TysX6mV z`L4EM$6<0LRB0uJt|H7VPR`6@-DjWaQ*oHKa93S@cbvTqE;fx|^L0<+R78(vkz6uH zjISR^QBANdr^ZFl_i-3@C;j`n`HN^4=PR_y$$%l!&I=S111lN`+FqLC_HZ}mcd743 z2TDI%-H~s(tTp;L6A9CF!jo;QX@A>~Dxc(B?mM9tLv*UCsiQY1pDmP>{C=;8f4t|x zbcDVeYIRj0$>uFO;B(5U z{DD9tZaiziIn?Uq{NQ;Wy-j1Owd7qOpvW7AMXM&fB>4~YCy$qP70Ormp!A}2?%iDaa3QT~Z zZPQ!0{&}9@LzQ7mH|Y2M=q74K5OiJ@4CJ@{pmb#)XOrb$zNS5T=n5{p#RnhFLb%*z z=$bO65jR4pqZSQnADg0k{<83IxqDa&od4(f-zVqiSbTNa$MuC9Zz!!>j(cpLczBp< zulIsjo;W5cz1AFW+K=a``vqjBe$f=`wycAyn(a7c7HL9pa~2LexbZ}u)3e-II?Tvy zX+_07OV-V(xXsmSTBI^x4X*VfgP(u6B(qZ_C-6)E27Q2#iXfi_%4#VRPvZX6D82j5 zH&@Y>3FU~D${P!vRmNtACB10_T-WYF1>J&T=UgTuC&03+#+@O7BN2CqrVHm!ymTw- zlvnPlrbY2)+OY#hSF8+sHUD+Melc=IGvk1!m%t21@{9+64kD20Sxp8Yh?FCk%WfuA zyGMG@U5a4T)yTSDXdakpT2O+|ehx0X0R%-17U;xq5?iiO&aclmIFt!$41KQuKqC2? zdU<)_E%~jAEB*n3%6mN# z7CsSMR99EW=dU^!nG-Fg_MnLJsi(Y-)j<;oXYy=ZUmncFeqSjBK&h0%3;L0G=ez(3 zOx6NP0Sx2VI$S@{qWkXdP?H|IQu+ddyw+}HWaX0cFLmosab|5&2^}JMZfj87u}2ce zke&|DyrrQXdYjAKXAPoRF6Uiuz5)Z9d%mo literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_collection_black.png b/src/main/res/drawable-xxxhdpi/ic_collection_black.png new file mode 100755 index 0000000000000000000000000000000000000000..4af96ccbff4d6f35779a18ddd1fe4aa83d055677 GIT binary patch literal 1508 zcmd5+=~L5J6urMhNMwn`BC>{1Tb8B{R8&+%63S{zV88{XkvLT&Xf$XcKqw?EV_gt% zQcyum9B^&XNZAc0D4|qr7@!jN6pRQY1QRhLi;c81{SP|*@a~&=_rCM)nS0(j9Z3zb zG_yAY0L!q@z$oJ`_^`01#&}lFSPNisCoGV%BW0iF$`{mFigi$)d*~zF%L{AoL5^Zs zQs){<{!7ZWh=h$iD|5Pe`L~$$VY~}I7hEX#H`9z4!v-C-ypT43i9IZC)ib>deQf3* zAwsl6b01&EGXWH@8v%Y_L?YxMx6t5TeRTw|=qLlefRWxy*0$FH5xN2_HXNVObtV5V?t+acFQNu~v zoJ>w8dlcZ2(z*Gv^@1}aLiy@oRL7%Pu^FMfIL9~6)`Wa~%uvqXX}#iPlb0EnAlL3> z5yhW8o^8fLn%_gWiaXa*e$eOQ;dOe(-7NI@r&C4*=+MvldC*q13|{p-2ZAv@XO%^$ zXJTfP7m6!>f1-mUHmrQsn4z5+>W}D}0o(VUE}(e5GRD8YRx0j^9hOd#(t5PwA7)4k<_XKjGx{%o<~DTWM3Q8$y;0g6ugGF)$DR00WCO7W6i@&`}HqNkOC0S;Rg zCHK=0XDSc|@c?T%JXT6^KkXv`rr)_wDIH>&%anGGbPWG;I}XbQ_a(Jep;1KGz9dF9 z*Z9=mTJ0H80x)_#@<$A z>{tJ3GjK>S87`~dlx)n^nXq3S*JL(~N2}h`T4r2F_d5F0i+ATP-6ey1ggp9jIyT-B>B+&v<)4emgvgwZC)B%x5wXkzrOx)~kl{7f@F8jkzyFaYMBkCSM zQk!uqsmtn3fIc;^8bk}WW(}agi@m6uLLv#2V+dUI%}h0bdp@xip#93j2OQvs6vP6& z_&sjO4}M$otI_UGj(KVcpu#`50dBp%n7x9l!=5k)$-wNBM$G>lUg^Wpi-ZM&+%-n` NhOi)NU}XS3>o3BwqEr9? literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_collection_blue.png b/src/main/res/drawable-xxxhdpi/ic_collection_blue.png new file mode 100755 index 0000000000000000000000000000000000000000..7fd5c9a898515c0cb94635e627e07d6f1b61edeb GIT binary patch literal 1673 zcmdUw>o?m67{~Mb`H_?$ai>zsTsoUO)iC0InMQ`u(;~E#a$47t4)Jq4Ro7PN#TwEw ztReKE)shM-opq`;rA<}bT2&*BAnumE*uSuI_TqV-7oX>HzHh$I6%x@?URFyMfk4Q6 zd%5{bD&wC($w+e1aP+qbgbc~s%{h<~wnV=ltLm&mh*n)0=}t+_k;%_+cGN9!ieTE- zW82l!rGI`C!`sR8a^#p{DvaN}-K=WDj9+=G(%YHNXN#8U?`g#DWWisztkTvEYR@T= zF$rJ2v>U6phLk|PY!d)V8SqPlo&v!j>QiCO45~ni1z}#QIt^ek$dCiN3=?XhzOF*2 z5`hrBqvA=>PB4IB*2a-i*lx-P0fY(zw1GrUB?5dr(_jbA!2jh!BeSUz_P~ou!}p8+ zyg_qTw87c@j+o@;_(x;YEvC4P_jDaKmO6enQhI{7@3?=-A^m#Q$HcHZOd>O-{PFT@ z-cQ>*v%7I}4aYjK)9c`@xwvRSKU%Z`tY(YY$quPzr^aYIL`qt$AdjY;AX=L_ZadX0Yb&-im^}1dl~c&s z@a8jyMpHxMMSh!i;@v}9cNR88H#|k}u=7E&M4GaB8fR8DgBkU9EjAe$LQYL6nXNYt zzhHeMD+(yI96ZSBpjvwu^_8x-Hi(33oGxm+jWb8i#044Bd>2&!Gi{b#Y#=uR6k-4f z(#!>0%*gNR)Nof7?4Ht2TSyuP=CsN?@c?dFAV2*{Ghl7EFBU>4Weo?kAMRjwkx%5q zLJ3OjwW_b)&i@gmv9ed)Kz5A{PU{M_PNQTJ&Vl>G67&JJ zzUCQ<174$^vHSlevy%+%-Y0vF%w&j(Elt0y&h5vdr$!(y+xb{d=I#%{^Qa(wZLxw` z@nwAt&Myaw({q!c?X6nY3v81|l?>VVWWlqdQENZFT=db0tn^FmT|(=LZd2bzS>WM%&v4d* zHt`XgcdxR!H_76N^J9~nlzpBaH^a2XT@|b*?w92~Ym0uQ?D)~{Ns6fW=!z6e5M?~;-fjUv)-pbLO#rG8>hW$OKrJZ)dNBh0oq|2@ivUWp%ZVNcAga5- z))0od1aBjK7`6;^JE%+`oMF__umpmp+1(O}^;wWc22r|Q+Ie!D{x8RTf literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_delete.png b/src/main/res/drawable-xxxhdpi/ic_delete.png new file mode 100755 index 0000000000000000000000000000000000000000..181fbdd6d5b92205375069700e1e80d0540ea522 GIT binary patch literal 1446 zcmeAS@N?(olHy`uVBq!ia0vp^3qY8I4M=vMPuFE&V9oV(aSW-5dppz6Z%U#-Te-L} zZ~2DH%mNuXIwtcD#rY>F9+XhdddRuuyI59YBI`qjTb1m}oNugVCtSHdL;TRWeV=Rp zJ8x2KOiZhImiUixneaV@1Qi3*Ys*Yn)Hpg=!k9QRj`|m}9AVUEWZCj)xdqdshN%op zQ6Gb47z+=0F)*(Eq1o49k)Ybp5cWsJ-9cu9kb{GEU6--~Uj(PZ0#W;;f&%PoSOhL~ z&40wv!91|W9jKR`k(;0Y{#g6`^%4Dl4H4HvP{=N_@ZPe^iVxm@OwHZ5P$B1#eTTvW$(|!;%|#~o-q_rrc&X^X-F-)u_pDxe z@v8aeEqsi(f8BIGu2tbN)`BbnoA1!#^J#4m{#! zX6)wo*m|5Tq-D?ibmpp`r(fkbbgn34klygE=0?NT=a=v9zxP9R$&P}<-zr$l%{CDbEsBCE#fv{@yVB$3zJbxZza8kriXDur&%1wvIPnfJvF@@kUyv4J)x;Z7 z1+;uC$Q8AAn}B|FN#T9Kw&l_FqdOT*7he+0IdJK*%*f8h8hq>?wM+imZH^@L@*%gA^6x7{rX#u*>8c2^=stcFV&iN=K1H{?(XjM^`rvsaHcSyef z{^;w9508JSR8`Ba|7&<+`<&AmEPuWx@7=>`Zf5rG__UqE^1J2qzt7L;Til+%@uS_m zdGq$Y{`*XN-Svx{JFY$FKB~gVGNps#gi1rh%#xG3z})NN;-JRJa%PElo+ksNsuGhC zr$T~SY>_t;hlZfw1fZDl%Sq9|{NCBo;KRf*!*lwbNeoP0E-WcR4hMX8J(gTe~DWM4f@vUgR literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_down.png b/src/main/res/drawable-xxxhdpi/ic_down.png new file mode 100755 index 0000000000000000000000000000000000000000..91b8d82d3f9028477831e95fe39341380a8057c6 GIT binary patch literal 1505 zcmeAS@N?(olHy`uVBq!ia0vp^3qY8I4M=vMPuFE&U|r$q;uum9_x8?Sza^<62R_dI zuyo^=4|*C64l8j6E8xbLOIQ&DcXvrMb zXSVNUEcDLz7fpX|bGZNiJju2cb^jUx4Uril_ughP`Lk#=1u-&C`?NczK}thZAVG8g zY1S2uqeAeIXcS|flarHEvt!$~Z;$WYyC=W&SNQbz@80dJD=PZ*@8ZRa-|x!D{*#xx z>}=b1jp@zfYP*>}^U4nBPn$N)&d$Q($J>P)@^__ISN}fS-QAtPdw=!!PoF-`);pB0 z>v|)oVe;9tXV34-eV1D?*hg_}s6Tt1-Qr31-s{&}n9c9oZ;Z7St^bwrTKb1?<bnK z)xP|_aNVZ;`~SaIVz9Qh&YwGf{`dEv_Zk=Y3D16iYtc%E-}+XYICn398^5dKp0%Cb zzuE+w|39~U{rc5>+Vtu5uafGjPyC#AuP)QGyei z*H67(v-!Wk?As038NTo2%-yH>```QXK;^%0wV2)i@M9Lg!Ee)rXT`hy8^Yrx*8>$) z)G*2Xw0ro*|G3%x9Y0ts9-E~Je^dUmMf<=e2*ZvP?OSE+WrUjKmgl1XL@&z84uWO)83I5tK2R?X+{3mBeD zx7L;0n4WtkouGYy(c|xO7oZ|@fvxrIi9eSAI@1Vpgo73nPr}*TEoSiom)UxnzpCfm z1Ui?)Pr88R)*H1gKi!mWH9UT}%StG5J){122Ka7TCg&7w+g>GJI zW~(X(mXFwqOr_YFuLhQ8H3)D`ys&bK|Jog400e79IMBfn!$69Q-$Tg8V&5v@PtiMD zb9Thl0#N=WG!5iI6m#&Ln*9shY1tJQz^Tzu&>&t;g@AjB@GY=}|0hnSKa!7*j;`tL zonnRlw)l9aQ>)byuvqM(XKGlr1E3b~Pvlq2=L{ z1dQX`6R{4B_CB<|Y~DlS=6_czH4IKqPYZn`U%iKcjP$L8i9{JacGHN!(6e(Xbkj_r zlT>xJ#3ay2mK=?Z=F<+mo|enlKAs1za6NZ`*M^|;b0e5 z8iJh`niEisM=raX%N=J4wJjUKhhtuu=c^8)8r2?0*HsDxKK%}&Z!~a2H2v}a9lN9{ym^5^p9$TLjqd=GT!T~QwGBr*0}sV?dqVAj;gAvjz~_QPp{{=eH|_{!~0KJ-2lWWkCyOVkE38J9H)UtZ|BEqtQ@-? z3P<1JlMFyEZ8#xd4JdoY*s@!bXfRw~6sCLnf$a;>6xG}SN#?EOg%@+QZ^rH2uvA?% z7vj(pcIyh*6`Su{bDGRGm7%tc&O?yOs)Q(WZl>ZafVoU8REX-&K0YbSKvuztI_I<1XVO91tD zh8ZBqPysGY0xy#!k+Zi&=w<@W4#-!E)&8yRbAhvl+MIlkpgetW(zfX2rPmgWAK7~K z7f4<@lScQ*eNt+X*jy$Hy%Jk2@&&NUO!@&r#mAz-)+nHP|8T2jpOs;4055)LI!-zV~Rc%^W7T~eg?R+vR zCII2C>_wgzTF)wI{J266T{yV>6N7rt4tQAY(}m?Xbw{N!tzWN)NHU8lNR?D9H7MQ{ zoZ9dD;F79kBYU$W*AO8aUVpS7sXg%{XdAtQ8YfkJ6`31D3C z;weD;nvD?SmXp3`%F5cQE1CGFySMneBK4hd*Re}+JOy+*$HZnH#MYZ(^yuP&RM=O3 zQQ_W|e}7v4$B~FvUnu|>Poru#fMtm-_v4}GLhN-6gLIN8D;?Dg8P#f{J literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_favorites_black.png b/src/main/res/drawable-xxxhdpi/ic_favorites_black.png new file mode 100755 index 0000000000000000000000000000000000000000..12cbf804c241679fc12acd207e103c735f4db398 GIT binary patch literal 2490 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 NMh_4M@9#Uu{TJJ&`nCW7 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_isbn.png b/src/main/res/drawable-xxxhdpi/ic_isbn.png new file mode 100755 index 0000000000000000000000000000000000000000..7d2256ebf8c1c656e8143635c42addbdf09cd07a GIT binary patch literal 3096 zcmd^>`8U*yAI3jpo5E0}GQ<>aC{zqt#x`Rt*~Z#sLR5?;WjB@?OR_ID*>^FR?1W0D zt}@7$;o9Pobp|oAi`(~a`2P0&{_s4{Z_hc;Iq&y5uQ+p4eF!&{8vp=^p#jR`cNYFf zC%ArlLcgmV0Psv3qI9f+99Hv!UrFf+eO`=+7eN=-muJ#05s3CER5X<+9UFgF$f|?$ zl+`SEoHhp=R0nb~6+r8wIZ-+#Ij~G4BFw$Z};}3&GoHTGD@eG z&x&_b7`ZeTt&shRS6*iGGYTaWbo~~srq=0+)2U2I+g-!EO zb?Gb8i>aOBF>@H^xCO@J(~r4~&!^nLmVCt;goW8bKr%VBU_vQ~Eb1zIOeC~F`P^QW zBFGxeXZDqpgTZovu&Cp+P7AuQ{gRHV#39e)!u&O$yFpz| zgacN$b7{&}+6&>?={HgyjyP_RNII?|Nw7wf`rECq^KXTR{0*)37m^Xx=9qp7T7w+Y zsl_#0T&be@x&!voW_Vddw==*Y z+;KrzBDClt5T3jHLxH9f#SJkHM-6&y8IrtYo?_C^VF^4*n(9ILxf?*}mE&E%2ytAQ z{*x;*ftK{-es`aN@-|d|RM-5+$lKNwV-R(k1yK>5vEOD(MH8TP@WAwtKDy#q-Da8f zdL3WE$=RLX`{mY)f;`TLzwae5##HV{uE9dx9x3*BTJD)81qAl(ww@6`I6G(G6#x6S zG`OF@T{CVVMAkB(2xa!mCP!|zIr;kSNVwXOaP zxLqZAfYM}3QquNS&(QkzY+?m_*yh4Ve6!_{9apl4`c~F}md7Lg9+%DX?GE{RWtu>! z?ZqAQq#BI8{V!FpPP-$B>N>k=E4*cPDMj#>eXIs&;gnGG822Hr7BfskMio4(^?t zP4w#0UHXp@dfKYJf zdxZ1}E_A;nyf_1-hwnjw^w08D`pI@@1N6#{9xCD2Ms7oW9wJ!gZ{4eJ!0V>>vVzSm zj(9NSt0G zdb2I@CfZU-_w9(n6#56i-6uc~F) zt79xOp+Rsq4%p#wVl6c)*9Cs$JO`z+rCC@Lbtx}~3(ZXFLjtc>aX}_3TdyO4qeLnm z5`_H0A&Yebz_Hw??5chtjn-ZVLLr0|qB%u=^<-h0{ZHc{HB~qE;g?^4ZO`NfeLpDJ z_aUF3{4wr`{?3%^hcQnUIx{j*lwkRbDP31PZJoJl!2-Q*zET1=j8XW)z&=;4xenBp z$&@c#>WC&S->D7Avkv=~ZbZa3(R3}56@#q{94eR1SCcoyV8+*U-X1}TL3^hgMpG{(DyeBP# z!d;F+*26@hve(F7w7BKU3U8l48V7NQ6uaAFh9HZIryYarXgm3@$X~ zLzEQ?`2{5KfuVnK)SD85I%wbD9=de0Exu`DEKPYxXYXUw8E&e1_p2O>&z;#FXrQFW z;V9afzoyv@N|@O5dHfCZ*GHZTX!-G>q6p+_z-hmbQ;vcwMK%Ws$=ma8xq~SS=03ZK zXz|6Abhp+LIN-^`^p|aNd5n)ycdD~NMqwhkf_wSJzdp92q8J8tP+)4A8y$GF#9 z%>GNSxb(F`n)gOLjzwRtwezWID1f_`>j`{C60VpE4vhn)`KBQVrGg;2d9<^!`?<+i zG!GO07P{Zfsjt#bKnB`XABUj=(4_x6?)z`x_n2q@lWQMB-yz~R@dJi>rl@LNr^tT+ DvRj}> literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_item_black.png b/src/main/res/drawable-xxxhdpi/ic_item_black.png new file mode 100755 index 0000000000000000000000000000000000000000..673c033b363866212540528147cd686fcd49af39 GIT binary patch literal 1645 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 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_launcher.png b/src/main/res/drawable-xxxhdpi/ic_launcher.png new file mode 100755 index 0000000000000000000000000000000000000000..c002e89f1487589304ce46a9afb9453c4c5b31ae GIT binary patch literal 7121 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 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_manual.png b/src/main/res/drawable-xxxhdpi/ic_manual.png new file mode 100755 index 0000000000000000000000000000000000000000..3507637ef5a452a131418b22e9f5208763e3786d GIT binary patch literal 1003 zcmeAS@N?(olHy`uVBq!ia0vp^3qY8I4M=vMPuFE&VBX~E;uum9_x9H9P-jOT<^!|; zb+=t`__WnM%0#3gH}g}&ldnG}Iv6yD7i_yIRKd`qFpHt#;tyW-35^*{3~rCjWgOTR zaWY8s-Y;N~>`-G!FsVDtm@?EA?U#FS{&)J$W7mH_fA)KQ+&}#a2F4yc21XWv0|`uV zP0bIhIqT$rLSX;aw=i%dm@qOd{wQ5n&2;_0{D*H$*S@nC#5(+_V>ryZrXM?VzubcS z!#~V?zBuwIG%&CUe+Y~*sEI%RsdSBj{r$F&JQ8XzY!`fQ2y9|t;z%%H)IWIeL+?KO z#@GJ^YwKBWi!4gHUr9{#pl@SeHt=TEx@=?4rP-!txI?-61VaA0WWtl;Ep%m4D1GvPAn^}+W7 zMzf^V0xb#=ovSw#i#M&l7#gvQO*(tF$s?hpMM`0I>aGfcs+sR>%a7@Qn2@mJ^Z#@^ z|DYM$@Bf_OV9;1wu5#Xy9=XW-yT2|B%5*z?b1b z#-BFk6AtuH6lYjqA#?BQa(?lYk89`st=WHnzP`octBV=;@O@ZfXB$`0oc{IAVy22+ z4|>A-*#r(~L@+S27|dd5xcI|Vp@G2->}oa^fdd~rCFH!v_sfZZ=8;J~n$lR=_4 zo|%aw0qm1(2L{F-HHHL}I$jQi2C&E1Gg8s8{WICtY(HKb>3OGr*~Z&@SN;4hW^?#_ z)wkDGe>-T7;9~<@lrmXG$f4taE^niQxPWy)Fn@6giGknO8 zyuLkwcL$#n17nQ)E&F8*K(|ik{2^(OrDznxoOOU>&*_B>OdJdH4+b({d?*#b%(Q}W zG25cUrVH3u0+`)+UD~r9csUlZ^+}oeWaNBo%bNrR)FLkj6ZmZpy zZ)C-O=Hq^_XYRILf(>i0nZDn^aPD^IYH7xyCGnTdX|ut8QHg^ufu#$Br>mdKI;Vst E0KIq0TmS$7 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_scan.png b/src/main/res/drawable-xxxhdpi/ic_scan.png new file mode 100755 index 0000000000000000000000000000000000000000..e6dbf41c67fe857faf3622e2ab887d5f189b470b GIT binary patch literal 1136 zcmeAS@N?(olHy`uVBq!ia0vp^3qY8I4M=vMPuFE&U@`Y}aSW-5dwchwZ*-zSYv3Mr z(Ew)A0EPg8&;Wq|1`y{+b$Z7n-z5(i_donJ&rjuj-Lq5ieUnso8`q0)h}~e_?yFtE zroeKMiNWo&H2Z|c3`T~>`wJN)JH!|a zX5F`F;Bnz*kT{z!dZPXAwBC zZ(8GY1}2LGtLlXu7=A~w#W8U_U|9Xna|CND;$<-Z%f00F^}}^9zrVixW7c$Fz(;7V z6F>DaWdHhK&-O;MCS9)PE{%C}Mv5Ws+VbP0l+XkKSBuAM literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_settings.png b/src/main/res/drawable-xxxhdpi/ic_settings.png new file mode 100755 index 0000000000000000000000000000000000000000..3a9d01cb5744ef32e2ff4ce090ac02dceb825209 GIT binary patch literal 2644 zcmdT`X*ARgAN|jat+8bpl_g3tMn*)iqp7+D={?57Qckj99{JvbOy&Xmpst5%DK+@XE z+;L|Y{XJqLJD4)ys{;T~C2Mmtr*O}&w6JG53we&55sm+808p^2uuu_w1BwK?D0DD*&^bnU55W#mk05>0AZWk`vOLjH|Sg$I_!S)hNgc@*(?@9*2MR2g)l!_&7CLj-~ zfQp&A{)XXzJD~rGEt@M*5T8+T+s|J1K_z*sixG(xJm=4X8NqePE=+M6f!2E5V98ng z1en1z8fl*hNF)C1(%L$g=ezn%2p@uCrZbz7czU}}2k-VT|7&Sw2^*sA|L)*eJW|Sw z2I6c^2%&W0&|M)72E$ezNhf4^bJawK}>a=LBY&a_AZ7u1mkQT=a{16kl9+Z zbSK0jyQYopE6om{zdH2qoU$Wo%llKXlD@)n*!oF!f`FSW6a=8bQo`@*BWvY(V%;Fjew? z`IYj`I}p)N>#{XgXizvxK3a&Qz;N^x=E?eKu1}$pGsP(h z=`};4J!V57_|ykD#CJk6s=)>cJOb%O5P#T2wN7d`YvRANQG~nz-JCU-#9~^*Z55Fz zZNu-Mod}#p9SAK5wCMb-2Pu-#d0<((1LZ!m$p({dMHO7^ZWmIX;on>HxpqE-t)~|Z zx?4GV^6+x=ZKfuMGV;R2tXaC=&!%KAKUyJ1%3$eXzbz8@CLtU?@1dJ3`!&6=^S~!g zD{^0ObV01 z#w)#~!cv8u_t^nU>r3rhmU}hRN)g5ZP0b+>TbAnHNmnKeupK|9quCE1jOkLU+A}TA zIa8mvoea>$1?XN=BiLLF@0uFi8su1}2F&n}9cZ#DQ#!1i^J+yRVxraH`( zVg!2TpTlISCXa>y_9}ls;v8cWdsIwY?!2Wlw|Y*Xu(xEA%!lwokQ}c|PZiUo8@A*XMREMymHSJ`RyoN3@E$#$V1i$=1eydvG7+b1x;$ z^`U`QuiFi735w10O*XdelW=%${s>n=(VZ|3vi8jAOxlInAH4oQD|iJ~8kKswF;%7*Vg5yLCwh+gFo#14hG& zJFRBf?7+Kc-<92;G&J4Zfq>eM^Fs&uv^tiE>ZSe2l2|TI@xbeIH}aFb{A+E`CT^b} z%}kuP(AIG^RT_4(f;4%fz^sUjap+5F9Ncf1Z?EybjBIt&&*cj+Mz-=BF{+hms3(TR zGy0Q;yNmj9qop6xr=6q9HN=7(rgF3B^Td~P2&WUNdazd3*9?wIuWtvB+QQI4uEQFw z1`oYlN?=U_Z!R)^UP)k})4Ehglk@2dVe6u~Ndu(`BFddn=+Kw7b110jOnaWvAzXDT zQUCnyK&@KOKKx?D{pjnF?T*AVi*BCjS+jL}WifBay(r}MUPC$>LPovr)|JEeiO7@< z>X#@O*D8De46N~Kjh~z}S2pT$w%TKEq*6huIEF$ljSemxk*jpWr7%WLThPyE&6@Lm zFfQXuMF3@YWJ6R4XwcF0lYqyxzQ=RJ7n?nQOaVxkD+i@s+nI|M8h$>sL@V(C@jU%T zskAiMJbxg(&wI+tA?xWOL@hwtlO-(}O(v&S&5&k;&Yq>e4oFS;JY@$c%x|5P`cA}Y z1WycHZ8M&#yfXhc*?S{%G_6-ZaF06I&w=1*3Mfpm%U#goT>YxC43AdCF7_h{8%wEB zFOS2ahX)up-9#_5zzQRi;mR|f%30LN95OHSeg(y3AG|5;i~|MH5clP`u&BnZ3JQ(r zrFBZt&&gq#Z!*XELg%q$D72O7)l!l{X5u1t4PTt}p$}09R|66g{_hLSf2~{Fz^nm| UF|fLU*=dV_wS}E|#Tk73Uq|)ROaK4? literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_star_empty.png b/src/main/res/drawable-xxxhdpi/ic_star_empty.png new file mode 100755 index 0000000000000000000000000000000000000000..a691a207086aa05d670290dc8d51c5f43ea9b43c GIT binary patch literal 4279 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 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_tag_black.png b/src/main/res/drawable-xxxhdpi/ic_tag_black.png new file mode 100755 index 0000000000000000000000000000000000000000..1d04cf0b45f7fe72f2e0b0d0228495e6618b6092 GIT binary patch literal 1848 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 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable/bg_light.xml b/src/main/res/drawable/bg_light.xml new file mode 100755 index 0000000..f91b837 --- /dev/null +++ b/src/main/res/drawable/bg_light.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/main/res/drawable/bg_primary_light.xml b/src/main/res/drawable/bg_primary_light.xml new file mode 100755 index 0000000..ab6d13b --- /dev/null +++ b/src/main/res/drawable/bg_primary_light.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/main/res/drawable/book.png b/src/main/res/drawable/book.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/book_open.png b/src/main/res/drawable/book_open.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/comment.png b/src/main/res/drawable/comment.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/email.png b/src/main/res/drawable/email.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/fab_label_background.xml b/src/main/res/drawable/fab_label_background.xml new file mode 100755 index 0000000..3ba2a5b --- /dev/null +++ b/src/main/res/drawable/fab_label_background.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/film.png b/src/main/res/drawable/film.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/folder.png b/src/main/res/drawable/folder.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/glyphish_02_redo.png b/src/main/res/drawable/glyphish_02_redo.png deleted file mode 100644 index f8c8fb1df21cb4d6df7f1f6651076e536905fee9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3315 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 diff --git a/src/main/res/drawable/glyphish_104_index_cards.png b/src/main/res/drawable/glyphish_104_index_cards.png deleted file mode 100644 index a1a5dfd0de47e47f593dd6ac85d679a286581ef7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2895 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 AoB#j- diff --git a/src/main/res/drawable/glyphish_10_medical.png b/src/main/res/drawable/glyphish_10_medical.png deleted file mode 100644 index ab78c78342dc9f7e8ef733d0f2bc5ed562e2f65d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2915 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 N002ovPDHLkV1mNJYG?oe diff --git a/src/main/res/drawable/glyphish_151_telescope.png b/src/main/res/drawable/glyphish_151_telescope.png deleted file mode 100644 index be5852d593972a05b7d6cbeabd2d21c65b9850e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 370 zcmV-&0ge8NP)6o6e;kMzrcfj{qvw~nx@GTMGd-w3eZj^ zcp|8ZCxUfuV3E)_>JEC4xs!UZNT_x#KoT_L_z22bB$UPhbi;nGKEM({b(}#TS{jxB zYQ8gIzu=A|Rj=F&iS0#?c?49V%Mn{?F5m&s7Gd+hg&b>Yi!V85rsLVih1$@bz&doc z1?@pE#~+#bIBy;E)k3cGb?yq`nOhAn$VKFnUj?rN=e*#hacXG4Oyztaxgmw~oNU}k z@*~;QJQPly^$Yzv6fTU*$aX=AP8U}fi7AzZCsS>JicCFS978H@C6yEw{yfjX*b^W%lRw#bL8szn9*ZlDM`uc8 q@VMzpY)!b*6vz!?Ft9K%@i0X0V1K_qg|8N90E4HipUXO@geCweLn_|@ diff --git a/src/main/res/drawable/glyphish_22_skull_n_bones.png b/src/main/res/drawable/glyphish_22_skull_n_bones.png deleted file mode 100644 index 462f54986e3c47a9d22a380f6c728dfdf2a36a57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3595 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 RX$AlQ002ovPDHLkV1kFYzkdJ# diff --git a/src/main/res/drawable/glyphish_59_flag.png b/src/main/res/drawable/glyphish_59_flag.png deleted file mode 100644 index 3be301471e75e3f2f719bd130c650f50c3b58be8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2954 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 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable/layout.png b/src/main/res/drawable/layout.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/list_child_indicator.xml b/src/main/res/drawable/list_child_indicator.xml old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/map.png b/src/main/res/drawable/map.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/newspaper.png b/src/main/res/drawable/newspaper.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/note.png b/src/main/res/drawable/note.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/page.png b/src/main/res/drawable/page.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/page_white.png b/src/main/res/drawable/page_white.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/page_white_acrobat.png b/src/main/res/drawable/page_white_acrobat.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/page_white_powerpoint.png b/src/main/res/drawable/page_white_powerpoint.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/page_white_text.png b/src/main/res/drawable/page_white_text.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/page_white_text_width.png b/src/main/res/drawable/page_white_text_width.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/page_white_width.png b/src/main/res/drawable/page_white_width.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/picture.png b/src/main/res/drawable/picture.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/report.png b/src/main/res/drawable/report.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/report_user.png b/src/main/res/drawable/report_user.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/script.png b/src/main/res/drawable/script.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/state_list_item.xml b/src/main/res/drawable/state_list_item.xml new file mode 100755 index 0000000..4c6dd1a --- /dev/null +++ b/src/main/res/drawable/state_list_item.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/television.png b/src/main/res/drawable/television.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/zandy72.png b/src/main/res/drawable/zandy72.png deleted file mode 100644 index 773aae6cc7b85a4bd3094a95b13a10c7be13acd4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4448 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 mPUmw)MV5mg^{(hAg8u`hCY+33&BQGL0000 + + + + + \ No newline at end of file diff --git a/src/main/res/layout/activity_main.xml b/src/main/res/layout/activity_main.xml new file mode 100755 index 0000000..5043b8e --- /dev/null +++ b/src/main/res/layout/activity_main.xml @@ -0,0 +1,214 @@ + + + + + + +