From bf133c65c388f93cd408d0cd9fc0599846f881c2 Mon Sep 17 00:00:00 2001 From: Jake McGinty Date: Wed, 6 May 2015 13:53:55 -0700 Subject: [PATCH] refactor emoji code into package 1) EmojiTextView and EmojiEditText are used instead of using code to emojify text. 2) Emoji categories' code points are specified in XML 3) EmojiDrawer itself is a fragment, and its pages are also fragments, allowing for better memory management. Fixes #2938 Fixes #2936 Closes #3153 // FREEBIE --- AndroidManifest.xml | 2 +- build.gradle | 3 +- res/layout/conversation_activity.xml | 10 +- res/layout/conversation_bubble_incoming.xml | 3 +- res/layout/conversation_bubble_outgoing.xml | 3 +- res/layout/conversation_list_item_view.xml | 3 +- res/layout/emoji_drawer.xml | 20 +- res/layout/emoji_drawer_stub.xml | 6 - res/values/attrs.xml | 1 - res/values/emoji.xml | 867 ++++++++++++++++++ .../securesms/ConversationActivity.java | 52 +- .../securesms/ConversationItem.java | 5 +- .../securesms/ConversationListItem.java | 6 +- .../securesms/components/ComposeText.java | 2 +- .../securesms/components/EmojiDrawer.java | 273 ------ .../components/emoji/EmojiDrawer.java | 156 ++++ .../components/emoji/EmojiEditText.java | 38 + .../components/emoji/EmojiPageFragment.java | 116 +++ .../components/emoji/EmojiPageModel.java | 7 + .../components/emoji/EmojiProvider.java | 257 ++++++ .../components/emoji/EmojiTextView.java | 34 + .../components/{ => emoji}/EmojiToggle.java | 2 +- .../emoji/EmojiTransformationMethod.java | 17 + .../emoji/RecentEmojiPageModel.java | 111 +++ .../emoji/StaticEmojiPageModel.java | 24 + .../thoughtcrime/securesms/util/Emoji.java | 474 ---------- .../thoughtcrime/securesms/util/ResUtil.java | 13 +- 27 files changed, 1695 insertions(+), 810 deletions(-) delete mode 100644 res/layout/emoji_drawer_stub.xml create mode 100644 res/values/emoji.xml delete mode 100644 src/org/thoughtcrime/securesms/components/EmojiDrawer.java create mode 100644 src/org/thoughtcrime/securesms/components/emoji/EmojiDrawer.java create mode 100644 src/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java create mode 100644 src/org/thoughtcrime/securesms/components/emoji/EmojiPageFragment.java create mode 100644 src/org/thoughtcrime/securesms/components/emoji/EmojiPageModel.java create mode 100644 src/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java create mode 100644 src/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java rename src/org/thoughtcrime/securesms/components/{ => emoji}/EmojiToggle.java (97%) create mode 100644 src/org/thoughtcrime/securesms/components/emoji/EmojiTransformationMethod.java create mode 100644 src/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java create mode 100644 src/org/thoughtcrime/securesms/components/emoji/StaticEmojiPageModel.java delete mode 100644 src/org/thoughtcrime/securesms/util/Emoji.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 2ae7bc12c3..05eb86c62b 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -5,7 +5,7 @@ android:versionCode="118" android:versionName="2.14.4"> - + - - + diff --git a/res/layout/conversation_bubble_incoming.xml b/res/layout/conversation_bubble_incoming.xml index d9e3999c13..6e0b4f0c0a 100644 --- a/res/layout/conversation_bubble_incoming.xml +++ b/res/layout/conversation_bubble_incoming.xml @@ -42,7 +42,8 @@ android:layout_below="@id/thumbnail_container" android:orientation="vertical"> - - - + android:layout_height="45dp" + app:pstsShouldExpand="true" + app:pstsTabPaddingLeftRight="@dimen/emoji_drawer_left_right_padding" + app:pstsUnderlineColor="@color/emoji_tab_underline" + app:pstsIndicatorColor="@color/emoji_tab_indicator" + app:pstsIndicatorHeight="@dimen/emoji_drawer_indicator_height" + app:pstsTextAllCaps="false"/> - + \ No newline at end of file diff --git a/res/layout/emoji_drawer_stub.xml b/res/layout/emoji_drawer_stub.xml deleted file mode 100644 index 72bf99b4f7..0000000000 --- a/res/layout/emoji_drawer_stub.xml +++ /dev/null @@ -1,6 +0,0 @@ - - \ No newline at end of file diff --git a/res/values/attrs.xml b/res/values/attrs.xml index d4e510c54c..58e787b36b 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -113,5 +113,4 @@ - diff --git a/res/values/emoji.xml b/res/values/emoji.xml new file mode 100644 index 0000000000..855d533606 --- /dev/null +++ b/res/values/emoji.xml @@ -0,0 +1,867 @@ + + + + 0x263a + 0x1f60a + 0x1f600 + 0x1f601 + 0x1f602 + 0x1f603 + 0x1f604 + 0x1f605 + 0x1f606 + 0x1f607 + 0x1f608 + 0x1f609 + 0x1f62f + 0x1f610 + 0x1f611 + 0x1f615 + 0x1f620 + 0x1f62c + 0x1f621 + 0x1f622 + 0x1f634 + 0x1f62e + 0x1f623 + 0x1f624 + 0x1f625 + 0x1f626 + 0x1f627 + 0x1f628 + 0x1f629 + 0x1f630 + 0x1f61f + 0x1f631 + 0x1f632 + 0x1f633 + 0x1f635 + 0x1f636 + 0x1f637 + 0x1f61e + 0x1f612 + 0x1f60d + 0x1f61b + 0x1f61c + 0x1f61d + 0x1f60b + 0x1f617 + 0x1f619 + 0x1f618 + 0x1f61a + 0x1f60e + 0x1f62d + 0x1f60c + 0x1f616 + 0x1f614 + 0x1f62a + 0x1f60f + 0x1f613 + 0x1f62b + 0x1f64b + 0x1f64c + 0x1f64d + 0x1f645 + 0x1f646 + 0x1f647 + 0x1f64e + 0x1f64f + 0x1f63a + 0x1f63c + 0x1f638 + 0x1f639 + 0x1f63b + 0x1f63d + 0x1f63f + 0x1f63e + 0x1f640 + 0x1f648 + 0x1f649 + 0x1f64a + 0x1f4a9 + 0x1f476 + 0x1f466 + 0x1f467 + 0x1f468 + 0x1f469 + 0x1f474 + 0x1f475 + 0x1f48f + 0x1f491 + 0x1f46a + 0x1f46b + 0x1f46c + 0x1f46d + 0x1f464 + 0x1f465 + 0x1f46e + 0x1f477 + 0x1f481 + 0x1f482 + 0x1f46f + 0x1f470 + 0x1f478 + 0x1f385 + 0x1f47c + 0x1f471 + 0x1f472 + 0x1f473 + 0x1f483 + 0x1f486 + 0x1f487 + 0x1f485 + 0x1f47b + 0x1f479 + 0x1f47a + 0x1f47d + 0x1f47e + 0x1f47f + 0x1f480 + 0x1f4aa + 0x1f440 + 0x1f442 + 0x1f443 + 0x1f463 + 0x1f444 + 0x1f445 + 0x1f48b + 0x2764 + 0x1f499 + 0x1f49a + 0x1f49b + 0x1f49c + 0x1f493 + 0x1f494 + 0x1f495 + 0x1f496 + 0x1f497 + 0x1f498 + 0x1f49d + 0x1f49e + 0x1f49f + 0x1f44d + 0x1f44e + 0x1f44c + 0x270a + 0x270c + 0x270b + 0x1f44a + 0x261d + 0x1f446 + 0x1f447 + 0x1f448 + 0x1f449 + 0x1f44b + 0x1f44f + 0x1f450 + + + + 0x1f530 + 0x1f484 + 0x1f45e + 0x1f45f + 0x1f451 + 0x1f452 + 0x1f3a9 + 0x1f393 + 0x1f453 + 0x231a + 0x1f454 + 0x1f455 + 0x1f456 + 0x1f457 + 0x1f458 + 0x1f459 + 0x1f460 + 0x1f461 + 0x1f462 + 0x1f45a + 0x1f45c + 0x1f4bc + 0x1f392 + 0x1f45d + 0x1f45b + 0x1f4b0 + 0x1f4b3 + 0x1f4b2 + 0x1f4b5 + 0x1f4b4 + 0x1f4b6 + 0x1f4b7 + 0x1f4b8 + 0x1f4b1 + 0x1f4b9 + 0x1f52b + 0x1f52a + 0x1f4a3 + 0x1f489 + 0x1f48a + 0x1f6ac + 0x1f514 + 0x1f515 + 0x1f6aa + 0x1f52c + 0x1f52d + 0x1f52e + 0x1f526 + 0x1f50b + 0x1f50c + 0x1f4dc + 0x1f4d7 + 0x1f4d8 + 0x1f4d9 + 0x1f4da + 0x1f4d4 + 0x1f4d2 + 0x1f4d1 + 0x1f4d3 + 0x1f4d5 + 0x1f4d6 + 0x1f4f0 + 0x1f4db + 0x1f383 + 0x1f384 + 0x1f380 + 0x1f381 + 0x1f382 + 0x1f388 + 0x1f386 + 0x1f387 + 0x1f389 + 0x1f38a + 0x1f38d + 0x1f38f + 0x1f38c + 0x1f390 + 0x1f38b + 0x1f38e + 0x1f4f1 + 0x1f4f2 + 0x1f4df + 0x260e + 0x1f4de + 0x1f4e0 + 0x1f4e6 + 0x2709 + 0x1f4e8 + 0x1f4e9 + 0x1f4ea + 0x1f4eb + 0x1f4ed + 0x1f4ec + 0x1f4ee + 0x1f4e4 + 0x1f4e5 + 0x1f4ef + 0x1f4e2 + 0x1f4e3 + 0x1f4e1 + 0x1f4ac + 0x1f4ad + 0x2712 + 0x270f + 0x1f4dd + 0x1f4cf + 0x1f4d0 + 0x1f4cd + 0x1f4cc + 0x1f4ce + 0x2702 + 0x1f4ba + 0x1f4bb + 0x1f4bd + 0x1f4be + 0x1f4bf + 0x1f4c6 + 0x1f4c5 + 0x1f4c7 + 0x1f4cb + 0x1f4c1 + 0x1f4c2 + 0x1f4c3 + 0x1f4c4 + 0x1f4ca + 0x1f4c8 + 0x1f4c9 + 0x26fa + 0x1f3a1 + 0x1f3a2 + 0x1f3a0 + 0x1f3aa + 0x1f3a8 + 0x1f3ac + 0x1f3a5 + 0x1f4f7 + 0x1f4f9 + 0x1f3a6 + 0x1f3ad + 0x1f3ab + 0x1f3ae + 0x1f3b2 + 0x1f3b0 + 0x1f0cf + 0x1f3b4 + 0x1f004 + 0x1f3af + 0x1f4fa + 0x1f4fb + 0x1f4c0 + 0x1f4fc + 0x1f3a7 + 0x1f3a4 + 0x1f3b5 + 0x1f3b6 + 0x1f3bc + 0x1f3bb + 0x1f3b9 + 0x1f3b7 + 0x1f3ba + 0x1f3b8 + 0x303d + + + + 0x1f415 + 0x1f436 + 0x1f429 + 0x1f408 + 0x1f431 + 0x1f400 + 0x1f401 + 0x1f42d + 0x1f439 + 0x1f422 + 0x1f407 + 0x1f430 + 0x1f413 + 0x1f414 + 0x1f423 + 0x1f424 + 0x1f425 + 0x1f426 + 0x1f40f + 0x1f411 + 0x1f410 + 0x1f43a + 0x1f403 + 0x1f402 + 0x1f404 + 0x1f42e + 0x1f434 + 0x1f417 + 0x1f416 + 0x1f437 + 0x1f43d + 0x1f438 + 0x1f40d + 0x1f43c + 0x1f427 + 0x1f418 + 0x1f428 + 0x1f412 + 0x1f435 + 0x1f406 + 0x1f42f + 0x1f43b + 0x1f42b + 0x1f42a + 0x1f40a + 0x1f433 + 0x1f40b + 0x1f41f + 0x1f420 + 0x1f421 + 0x1f419 + 0x1f41a + 0x1f42c + 0x1f40c + 0x1f41b + 0x1f41c + 0x1f41d + 0x1f41e + 0x1f432 + 0x1f409 + 0x1f43e + 0x1f378 + 0x1f37a + 0x1f37b + 0x1f377 + 0x1f379 + 0x1f376 + 0x2615 + 0x1f375 + 0x1f37c + 0x1f374 + 0x1f368 + 0x1f367 + 0x1f366 + 0x1f369 + 0x1f370 + 0x1f36a + 0x1f36b + 0x1f36c + 0x1f36d + 0x1f36e + 0x1f36f + 0x1f373 + 0x1f354 + 0x1f35f + 0x1f35d + 0x1f355 + 0x1f356 + 0x1f357 + 0x1f364 + 0x1f363 + 0x1f371 + 0x1f35e + 0x1f35c + 0x1f359 + 0x1f35a + 0x1f35b + 0x1f372 + 0x1f365 + 0x1f362 + 0x1f361 + 0x1f358 + 0x1f360 + 0x1f34c + 0x1f34e + 0x1f34f + 0x1f34a + 0x1f34b + 0x1f344 + 0x1f345 + 0x1f346 + 0x1f347 + 0x1f348 + 0x1f349 + 0x1f350 + 0x1f351 + 0x1f352 + 0x1f353 + 0x1f34d + 0x1f330 + 0x1f331 + 0x1f332 + 0x1f333 + 0x1f334 + 0x1f335 + 0x1f337 + 0x1f338 + 0x1f339 + 0x1f340 + 0x1f341 + 0x1f342 + 0x1f343 + 0x1f33a + 0x1f33b + 0x1f33c + 0x1f33d + 0x1f33e + 0x1f33f + 0x2600 + 0x1f308 + 0x26c5 + 0x2601 + 0x1f301 + 0x1f302 + 0x2614 + 0x1f4a7 + 0x26a1 + 0x1f300 + 0x2744 + 0x26c4 + 0x1f319 + 0x1f31e + 0x1f31d + 0x1f31a + 0x1f31b + 0x1f31c + 0x1f311 + 0x1f312 + 0x1f313 + 0x1f314 + 0x1f315 + 0x1f316 + 0x1f317 + 0x1f318 + 0x1f391 + 0x1f304 + 0x1f305 + 0x1f307 + 0x1f306 + 0x1f303 + 0x1f30c + 0x1f309 + 0x1f30a + 0x1f30b + 0x1f30e + 0x1f30f + 0x1f30d + 0x1f310 + + + + 0x1f3e0 + 0x1f3e1 + 0x1f3e2 + 0x1f3e3 + 0x1f3e4 + 0x1f3e5 + 0x1f3e6 + 0x1f3e7 + 0x1f3e8 + 0x1f3e9 + 0x1f3ea + 0x1f3eb + 0x26ea + 0x26f2 + 0x1f3ec + 0x1f3ef + 0x1f3f0 + 0x1f3ed + 0x1f5fb + 0x1f5fc + 0x1f5fd + 0x1f5fe + 0x1f5ff + 0x2693 + 0x1f3ee + 0x1f488 + 0x1f527 + 0x1f528 + 0x1f529 + 0x1f6bf + 0x1f6c1 + 0x1f6c0 + 0x1f6bd + 0x1f6be + 0x1f3bd + 0x1f3a3 + 0x1f3b1 + 0x1f3b3 + 0x26be + 0x26f3 + 0x1f3be + 0x26bd + 0x1f3bf + 0x1f3c0 + 0x1f3c1 + 0x1f3c2 + 0x1f3c3 + 0x1f3c4 + 0x1f3c6 + 0x1f3c7 + 0x1f40e + 0x1f3c8 + 0x1f3c9 + 0x1f3ca + 0x1f682 + 0x1f683 + 0x1f684 + 0x1f685 + 0x1f686 + 0x1f687 + 0x24c2 + 0x1f688 + 0x1f68a + 0x1f68b + 0x1f68c + 0x1f68d + 0x1f68e + 0x1f68f + 0x1f690 + 0x1f691 + 0x1f692 + 0x1f693 + 0x1f694 + 0x1f695 + 0x1f696 + 0x1f697 + 0x1f698 + 0x1f699 + 0x1f69a + 0x1f69b + 0x1f69c + 0x1f69d + 0x1f69e + 0x1f69f + 0x1f6a0 + 0x1f6a1 + 0x1f6a2 + 0x1f6a3 + 0x1f681 + 0x2708 + 0x1f6c2 + 0x1f6c3 + 0x1f6c4 + 0x1f6c5 + 0x26f5 + 0x1f6b2 + 0x1f6b3 + 0x1f6b4 + 0x1f6b5 + 0x1f6b7 + 0x1f6b8 + 0x1f689 + 0x1f680 + 0x1f6a4 + 0x1f6b6 + 0x26fd + 0x1f17f + 0x1f6a5 + 0x1f6a6 + 0x1f6a7 + 0x1f6a8 + 0x2668 + 0x1f48c + 0x1f48d + 0x1f48e + 0x1f490 + 0x1f492 + 0xfe4e5 + 0xfe4e6 + 0xfe4e7 + 0xfe4e8 + 0xfe4e9 + 0xfe4ea + 0xfe4eb + 0xfe4ec + 0xfe4ed + 0xfe4ee + + + + 0x1f51d + 0x1f519 + 0x1f51b + 0x1f51c + 0x1f51a + 0x23f3 + 0x231b + 0x23f0 + 0x2648 + 0x2649 + 0x264a + 0x264b + 0x264c + 0x264d + 0x264e + 0x264f + 0x2650 + 0x2651 + 0x2652 + 0x2653 + 0x26ce + 0x1f531 + 0x1f52f + 0x1f6bb + 0x1f6ae + 0x1f6af + 0x1f6b0 + 0x1f6b1 + 0x1f170 + 0x1f171 + 0x1f18e + 0x1f17e + 0x1f4ae + 0x1f4af + 0x1f520 + 0x1f521 + 0x1f522 + 0x1f523 + 0x1f524 + 0x27bf + 0x1f4f6 + 0x1f4f3 + 0x1f4f4 + 0x1f4f5 + 0x1f6b9 + 0x1f6ba + 0x1f6bc + 0x267f + 0x267b + 0x1f6ad + 0x1f6a9 + 0x26a0 + 0x1f201 + 0x1f51e + 0x26d4 + 0x1f192 + 0x1f197 + 0x1f195 + 0x1f198 + 0x1f199 + 0x1f193 + 0x1f196 + 0x1f19a + 0x1f232 + 0x1f233 + 0x1f234 + 0x1f235 + 0x1f236 + 0x1f237 + 0x1f238 + 0x1f239 + 0x1f202 + 0x1f23a + 0x1f250 + 0x1f251 + 0x3299 + 0x00ae + 0x00a9 + 0x2122 + 0x1f21a + 0x1f22f + 0x3297 + 0x2b55 + 0x274c + 0x274e + 0x2139 + 0x1f6ab + 0x2705 + 0x2714 + 0x1f517 + 0x2734 + 0x2733 + 0x2795 + 0x2796 + 0x2716 + 0x2797 + 0x1f4a0 + 0x1f4a1 + 0x1f4a4 + 0x1f4a2 + 0x1f525 + 0x1f4a5 + 0x1f4a8 + 0x1f4a6 + 0x1f4ab + 0x1f55b + 0x1f567 + 0x1f550 + 0x1f55c + 0x1f551 + 0x1f55d + 0x1f552 + 0x1f55e + 0x1f553 + 0x1f55f + 0x1f554 + 0x1f560 + 0x1f555 + 0x1f561 + 0x1f556 + 0x1f562 + 0x1f557 + 0x1f563 + 0x1f558 + 0x1f564 + 0x1f559 + 0x1f565 + 0x1f55a + 0x1f566 + 0x2195 + 0x2b06 + 0x2197 + 0x27a1 + 0x2198 + 0x2b07 + 0x2199 + 0x2b05 + 0x2196 + 0x2194 + 0x2934 + 0x2935 + 0x23ea + 0x23eb + 0x23ec + 0x23e9 + 0x25c0 + 0x25b6 + 0x1f53d + 0x1f53c + 0x2747 + 0x2728 + 0x1f534 + 0x1f535 + 0x26aa + 0x26ab + 0x1f533 + 0x1f532 + 0x2b50 + 0x1f31f + 0x1f320 + 0x25ab + 0x25aa + 0x25fd + 0x25fe + 0x25fb + 0x25fc + 0x2b1c + 0x2b1b + 0x1f538 + 0x1f539 + 0x1f536 + 0x1f537 + 0x1f53a + 0x1f53b + 0x1f51f + + 0x2754 + 0x2753 + 0x2755 + 0x2757 + 0x203c + 0x2049 + 0x3030 + 0x27b0 + 0x2660 + 0x2665 + 0x2663 + 0x2666 + 0x1f194 + 0x1f511 + 0x21a9 + 0x1f191 + 0x1f50d + 0x1f512 + 0x1f513 + 0x21aa + 0x1f510 + 0x2611 + 0x1f518 + 0x1f50e + 0x1f516 + 0x1f50f + 0x1f503 + 0x1f500 + 0x1f501 + 0x1f502 + 0x1f504 + 0x1f4e7 + 0x1f505 + 0x1f506 + 0x1f507 + 0x1f508 + 0x1f509 + 0x1f50a + + + + @array/emoji_smile + @array/emoji_flower + @array/emoji_bell + @array/emoji_car + @array/emoji_symbol + + + + @drawable/emoji_category_smile + @drawable/emoji_category_flower + @drawable/emoji_category_bell + @drawable/emoji_category_car + @drawable/emoji_category_symbol + + \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index ad9c0010bf..d656d9ad86 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -42,7 +42,6 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnFocusChangeListener; import android.view.View.OnKeyListener; -import android.view.ViewStub; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.TextView; @@ -53,8 +52,9 @@ import com.google.protobuf.ByteString; import org.thoughtcrime.securesms.TransportOptions.OnTransportChangedListener; import org.thoughtcrime.securesms.components.ComposeText; -import org.thoughtcrime.securesms.components.EmojiDrawer; -import org.thoughtcrime.securesms.components.EmojiToggle; +import org.thoughtcrime.securesms.components.emoji.EmojiDrawer; +import org.thoughtcrime.securesms.components.emoji.EmojiProvider; +import org.thoughtcrime.securesms.components.emoji.EmojiToggle; import org.thoughtcrime.securesms.components.SendButton; import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; @@ -93,7 +93,6 @@ import org.thoughtcrime.securesms.util.Dialogs; import org.thoughtcrime.securesms.util.DirectoryHelper; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicTheme; -import org.thoughtcrime.securesms.util.Emoji; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; @@ -323,8 +322,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity @Override public void onBackPressed() { - if (emojiDrawer.isPresent() && emojiDrawer.get().getVisibility() == View.VISIBLE) { - emojiDrawer.get().setVisibility(View.GONE); + if (isEmojiDrawerOpen()) { + getEmojiDrawer().hide(); emojiToggle.toggle(); } else { super.onBackPressed(); @@ -621,15 +620,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity @Override protected void onPostExecute(List drafts) { - boolean nativeEmojiSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; - Context context = ConversationActivity.this; - for (Draft draft : drafts) { - if (draft.getType().equals(Draft.TEXT) && !nativeEmojiSupported) { - composeText.setText(Emoji.getInstance(context).emojify(draft.getValue(), - new Emoji.InvalidatingPageLoadedListener(composeText)), - TextView.BufferType.SPANNABLE); - } else if (draft.getType().equals(Draft.TEXT)) { + if (draft.getType().equals(Draft.TEXT)) { composeText.setText(draft.getValue()); } else if (draft.getType().equals(Draft.IMAGE)) { addAttachmentImage(Uri.parse(draft.getValue())); @@ -713,10 +705,21 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity emojiToggle.setOnClickListener(new EmojiToggleListener()); } - private EmojiDrawer initializeEmojiDrawer() { - EmojiDrawer emojiDrawer = (EmojiDrawer)((ViewStub)findViewById(R.id.emoji_drawer_stub)).inflate(); - emojiDrawer.setComposeEditText(composeText); - return emojiDrawer; + private EmojiDrawer getEmojiDrawer() { + if (emojiDrawer.isPresent()) return emojiDrawer.get(); + + EmojiDrawer emojiDrawerFragment = EmojiDrawer.newInstance(); + emojiDrawerFragment.setComposeEditText(composeText); + getSupportFragmentManager().beginTransaction() + .replace(R.id.emoji_drawer, emojiDrawerFragment) + .commit(); + getSupportFragmentManager().executePendingTransactions(); + emojiDrawer = Optional.of(emojiDrawerFragment); + return emojiDrawerFragment; + } + + private boolean isEmojiDrawerOpen() { + return emojiDrawer.isPresent() && emojiDrawer.get().isOpen(); } private void initializeResources() { @@ -1094,16 +1097,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity public void onClick(View v) { InputMethodManager input = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); - if (emojiDrawer.isPresent() && emojiDrawer.get().isOpen()) { + if (isEmojiDrawerOpen()) { input.showSoftInput(composeText, 0); - emojiDrawer.get().hide(); + getEmojiDrawer().hide(); } else { - if (!emojiDrawer.isPresent()) { - emojiDrawer = Optional.of(initializeEmojiDrawer()); - } input.hideSoftInputFromWindow(composeText.getWindowToken(), 0); - emojiDrawer.get().show(); + getEmojiDrawer().show(); } } } @@ -1141,7 +1141,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity @Override public void onClick(View v) { - if (emojiDrawer.isPresent() && emojiDrawer.get().isOpen()) { + if (isEmojiDrawerOpen()) { emojiToggle.performClick(); } } @@ -1157,7 +1157,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity @Override public void onFocusChange(View v, boolean hasFocus) { - if (hasFocus && emojiDrawer.isPresent() && emojiDrawer.get().isOpen()) { + if (hasFocus && isEmojiDrawerOpen()) { emojiToggle.performClick(); } } diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java index a413c17a51..188ddeca5e 100644 --- a/src/org/thoughtcrime/securesms/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/ConversationItem.java @@ -53,7 +53,6 @@ import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.DateUtils; -import org.thoughtcrime.securesms.util.Emoji; import java.util.Locale; import java.util.Set; @@ -239,9 +238,7 @@ public class ConversationItem extends LinearLayout { if (isCaptionlessMms(messageRecord)) { bodyText.setVisibility(View.GONE); } else { - bodyText.setText(Emoji.getInstance(context).emojify(messageRecord.getDisplayBody(), - new Emoji.InvalidatingPageLoadedListener(bodyText)), - TextView.BufferType.SPANNABLE); + bodyText.setText(messageRecord.getDisplayBody()); bodyText.setVisibility(View.VISIBLE); } diff --git a/src/org/thoughtcrime/securesms/ConversationListItem.java b/src/org/thoughtcrime/securesms/ConversationListItem.java index ff3b9664a8..5840cbc514 100644 --- a/src/org/thoughtcrime/securesms/ConversationListItem.java +++ b/src/org/thoughtcrime/securesms/ConversationListItem.java @@ -31,7 +31,6 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.util.DateUtils; -import org.thoughtcrime.securesms.util.Emoji; import java.util.Locale; import java.util.Set; @@ -96,10 +95,7 @@ public class ConversationListItem extends RelativeLayout this.recipients.addListener(this); this.fromView.setText(recipients, read); - this.subjectView.setText(Emoji.getInstance(context).emojify(thread.getDisplayBody(), - Emoji.EMOJI_SMALL, - new Emoji.InvalidatingPageLoadedListener(subjectView)), - TextView.BufferType.SPANNABLE); + this.subjectView.setText(thread.getDisplayBody()); this.subjectView.setTypeface(read ? LIGHT_TYPEFACE : BOLD_TYPEFACE); if (thread.getDate() > 0) { diff --git a/src/org/thoughtcrime/securesms/components/ComposeText.java b/src/org/thoughtcrime/securesms/components/ComposeText.java index f72485423b..0099ccb734 100644 --- a/src/org/thoughtcrime/securesms/components/ComposeText.java +++ b/src/org/thoughtcrime/securesms/components/ComposeText.java @@ -9,7 +9,7 @@ import android.text.TextUtils; import android.text.style.RelativeSizeSpan; import android.util.AttributeSet; -public class ComposeText extends AppCompatEditText { +public class ComposeText extends EmojiEditText { public ComposeText(Context context) { super(context); } diff --git a/src/org/thoughtcrime/securesms/components/EmojiDrawer.java b/src/org/thoughtcrime/securesms/components/EmojiDrawer.java deleted file mode 100644 index fa2af88520..0000000000 --- a/src/org/thoughtcrime/securesms/components/EmojiDrawer.java +++ /dev/null @@ -1,273 +0,0 @@ -package org.thoughtcrime.securesms.components; - -import android.annotation.TargetApi; -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.os.Build.VERSION_CODES; -import android.support.v4.view.PagerAdapter; -import android.support.v4.view.ViewPager; -import android.util.AttributeSet; -import android.util.Log; -import android.util.Pair; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AbsListView; -import android.widget.AdapterView; -import android.widget.BaseAdapter; -import android.widget.EditText; -import android.widget.FrameLayout; -import android.widget.GridView; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; - -import com.astuetz.PagerSlidingTabStrip; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.components.RepeatableImageKey.KeyEventListener; -import org.thoughtcrime.securesms.util.Emoji; - -public class EmojiDrawer extends KeyboardAwareLinearLayout { - private static final KeyEvent DELETE_KEY_EVENT = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL); - - private static final int RECENT_TYPE = 0; - private static final int ALL_TYPE = 1; - - - private FrameLayout[] gridLayouts = new FrameLayout[Emoji.PAGES.length+1]; - private EditText composeText; - private Emoji emoji; - private ViewPager pager; - private PagerSlidingTabStrip strip; - - @SuppressWarnings("unused") - public EmojiDrawer(Context context) { - super(context); - initialize(); - } - - @SuppressWarnings("unused") - public EmojiDrawer(Context context, AttributeSet attrs) { - super(context, attrs); - initialize(); - } - - @SuppressWarnings("unused") - @TargetApi(VERSION_CODES.HONEYCOMB) - public EmojiDrawer(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - initialize(); - } - - public void setComposeEditText(EditText composeText) { - this.composeText = composeText; - } - - public boolean isOpen() { - return getVisibility() == View.VISIBLE; - } - - private void initialize() { - LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(R.layout.emoji_drawer, this, true); - - initializeResources(); - - if (!this.isInEditMode()) { - initializeEmojiGrid(); - } - } - - private void initializeResources() { - this.pager = (ViewPager) findViewById(R.id.emoji_pager); - this.strip = (PagerSlidingTabStrip) findViewById(R.id.tabs); - this.emoji = Emoji.getInstance(getContext()); - - RepeatableImageKey backspace = (RepeatableImageKey)findViewById(R.id.backspace); - backspace.setOnKeyEventListener(new KeyEventListener() { - @Override public void onKeyEvent() { - if (composeText != null && composeText.getText().length() > 0) { - composeText.dispatchKeyEvent(DELETE_KEY_EVENT); - } - } - }); - } - - public void hide() { - setVisibility(View.GONE); - } - - public void show() { - int keyboardHeight = getKeyboardHeight(); - Log.w("EmojiDrawer", "setting emoji drawer to height " + keyboardHeight); - setLayoutParams(new LinearLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, keyboardHeight)); - requestLayout(); - setVisibility(View.VISIBLE); - } - - private void initializeEmojiGrid() { - LayoutInflater inflater = LayoutInflater.from(getContext()); - for (int i = 0; i < gridLayouts.length; i++) { - gridLayouts[i] = (FrameLayout) inflater.inflate(R.layout.emoji_grid_layout, pager, false); - final GridView gridView = (GridView) gridLayouts[i].findViewById(R.id.emoji); - gridLayouts[i].setTag(gridView); - final int type = (i == 0 ? RECENT_TYPE : ALL_TYPE); - gridView.setColumnWidth(getResources().getDimensionPixelSize(R.dimen.emoji_drawer_size) + 2*getResources().getDimensionPixelSize(R.dimen.emoji_drawer_item_padding)); - gridView.setAdapter(new EmojiGridAdapter(type, i-1)); - gridView.setOnItemClickListener(new EmojiClickListener(ALL_TYPE)); - } - - pager.setAdapter(new EmojiPagerAdapter()); - - if (emoji.getRecentlyUsedAssetCount() <= 0) { - pager.setCurrentItem(1); - } - strip.setTabPaddingLeftRight(getResources().getDimensionPixelSize(R.dimen.emoji_drawer_left_right_padding)); - strip.setAllCaps(false); - strip.setShouldExpand(true); - strip.setUnderlineColorResource(R.color.emoji_tab_underline); - strip.setIndicatorColorResource(R.color.emoji_tab_indicator); - strip.setIndicatorHeight(getResources().getDimensionPixelSize(R.dimen.emoji_drawer_indicator_height)); - - strip.setViewPager(pager); - } - - private class EmojiClickListener implements AdapterView.OnItemClickListener { - - private final int type; - - public EmojiClickListener(int type) { - this.type = type; - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - Integer unicodePoint = (Integer) view.getTag(); - insertEmoji(composeText, unicodePoint); - if (type != RECENT_TYPE) { - emoji.setRecentlyUsed(Integer.toHexString(unicodePoint)); - ((BaseAdapter)((GridView)gridLayouts[0].getTag()).getAdapter()).notifyDataSetChanged(); - } - } - - private void insertEmoji(EditText editText, Integer unicodePoint) { - final char[] chars = Character.toChars(unicodePoint); - String characters = new String(chars); - int start = editText.getSelectionStart(); - int end = editText.getSelectionEnd(); - - CharSequence text = emoji.emojify(characters, new Emoji.InvalidatingPageLoadedListener(composeText)); - editText.getText().replace(Math.min(start, end), Math.max(start, end), text, 0, text.length()); - - editText.setSelection(end+chars.length); - } - } - - - - private class EmojiGridAdapter extends BaseAdapter { - - private final int type; - private final int page; - private final int emojiSize; - - public EmojiGridAdapter(int type, int page) { - this.type = type; - this.page = page; - emojiSize = (int) getResources().getDimension(R.dimen.emoji_drawer_size); - } - - @Override - public int getCount() { - if (type == RECENT_TYPE) return emoji.getRecentlyUsedAssetCount(); - else return Emoji.PAGES[page].length; - } - - @Override - public Object getItem(int position) { - return null; - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(final int position, final View convertView, final ViewGroup parent) { - final ImageView view; - final int pad = getResources().getDimensionPixelSize(R.dimen.emoji_drawer_item_padding); - if (convertView != null && convertView instanceof ImageView) { - view = (ImageView) convertView; - } else { - ImageView imageView = new ImageView(getContext()); - imageView.setPadding(pad, pad, pad, pad); - imageView.setLayoutParams(new AbsListView.LayoutParams(emojiSize + 2*pad, emojiSize + 2*pad)); - view = imageView; - } - - final Drawable drawable; - final Integer unicodeTag; - if (type == ALL_TYPE) { - unicodeTag = Emoji.PAGES[page][position]; - drawable = emoji.getEmojiDrawable(new Emoji.DrawInfo(page, position), - Emoji.EMOJI_HUGE, - new Emoji.InvalidatingPageLoadedListener(view)); - } else { - Pair recentlyUsed = emoji.getRecentlyUsed(position, - Emoji.EMOJI_HUGE, - new Emoji.InvalidatingPageLoadedListener(view)); - unicodeTag = recentlyUsed.first; - drawable = recentlyUsed.second; - } - - view.setImageDrawable(drawable); - view.setPadding(pad, pad, pad, pad); - view.setTag(unicodeTag); - return view; - } - } - - private class EmojiPagerAdapter extends PagerAdapter implements PagerSlidingTabStrip.IconTabProvider { - - @Override - public int getCount() { - return gridLayouts.length; - } - - @Override - public boolean isViewFromObject(View view, Object o) { - return view == o; - } - - public Object instantiateItem(ViewGroup container, int position) { - if (position < 0 || position >= gridLayouts.length) - throw new AssertionError("position out of range!"); - - container.addView(gridLayouts[position], 0); - - return gridLayouts[position]; - } - - @Override - public void destroyItem(ViewGroup container, int position, Object object) { - Log.w("EmojiDrawer", "destroying item at " + position); - container.removeView(gridLayouts[position]); - } - - @Override - public int getPageIconResId(int i) { - switch (i) { - case 0: return R.drawable.emoji_category_recent; - case 1: return R.drawable.emoji_category_smile; - case 2: return R.drawable.emoji_category_flower; - case 3: return R.drawable.emoji_category_bell; - case 4: return R.drawable.emoji_category_car; - case 5: return R.drawable.emoji_category_symbol; - default: return 0; - } - } - } -} diff --git a/src/org/thoughtcrime/securesms/components/emoji/EmojiDrawer.java b/src/org/thoughtcrime/securesms/components/emoji/EmojiDrawer.java new file mode 100644 index 0000000000..40bd7945c5 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/emoji/EmojiDrawer.java @@ -0,0 +1,156 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.ArrayRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.view.ViewPager; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; + +import com.astuetz.PagerSlidingTabStrip; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout; +import org.thoughtcrime.securesms.components.RepeatableImageKey; +import org.thoughtcrime.securesms.components.RepeatableImageKey.KeyEventListener; +import org.thoughtcrime.securesms.components.emoji.EmojiPageFragment.EmojiSelectionListener; +import org.thoughtcrime.securesms.util.ResUtil; + +import java.util.LinkedList; +import java.util.List; + +public class EmojiDrawer extends Fragment { + private static final KeyEvent DELETE_KEY_EVENT = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL); + + private EmojiEditText composeText; + private KeyboardAwareLinearLayout container; + private ViewPager pager; + private PagerSlidingTabStrip strip; + + public static EmojiDrawer newInstance(@ArrayRes int categories, @ArrayRes int icons) { + final EmojiDrawer fragment = new EmojiDrawer(); + final Bundle args = new Bundle(); + args.putInt("categories", categories); + args.putInt("icons", icons); + fragment.setArguments(args); + return fragment; + } + + public static EmojiDrawer newInstance() { + return newInstance(R.array.emoji_categories, R.array.emoji_category_icons); + } + + public void setComposeEditText(EmojiEditText composeText) { + this.composeText = composeText; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final View v = inflater.inflate(R.layout.emoji_drawer, container, false); + initializeResources(v); + initializeEmojiGrid(); + return v; + } + + private void initializeResources(View v) { + Log.w("EmojiDrawer", "initializeResources()"); + this.container = (KeyboardAwareLinearLayout) v.findViewById(R.id.container); + this.pager = (ViewPager) v.findViewById(R.id.emoji_pager); + this.strip = (PagerSlidingTabStrip) v.findViewById(R.id.tabs); + + RepeatableImageKey backspace = (RepeatableImageKey)v.findViewById(R.id.backspace); + backspace.setOnKeyEventListener(new KeyEventListener() { + @Override public void onKeyEvent() { + if (composeText != null && composeText.getText().length() > 0) { + composeText.dispatchKeyEvent(DELETE_KEY_EVENT); + } + } + }); + } + + public void hide() { + container.setVisibility(View.GONE); + } + + public void show() { + int keyboardHeight = container.getKeyboardHeight(); + Log.w("EmojiDrawer", "setting emoji drawer to height " + keyboardHeight); + container.setLayoutParams(new LinearLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, keyboardHeight)); + container.requestLayout(); + container.setVisibility(View.VISIBLE); + } + + public boolean isOpen() { + return container.getVisibility() == View.VISIBLE; + } + + private void initializeEmojiGrid() { + + pager.setAdapter(new EmojiPagerAdapter(getActivity(), + getFragmentManager(), + getPageModels(getArguments().getInt("categories"), + getArguments().getInt("icons")), + new EmojiSelectionListener() { + @Override public void onEmojiSelected(int emojiCode) { + composeText.insertEmoji(emojiCode); + } + })); + strip.setViewPager(pager); + } + + private List getPageModels(@ArrayRes int pagesRes, @ArrayRes int iconsRes) { + final int[] icons = ResUtil.getResourceIds(getActivity(), iconsRes); + final int[] pages = ResUtil.getResourceIds(getActivity(), pagesRes); + final List models = new LinkedList<>(); + models.add(new RecentEmojiPageModel(getActivity())); + for (int i = 0; i < icons.length; i++) { + models.add(new StaticEmojiPageModel(icons[i], getResources().getIntArray(pages[i]))); + } + return models; + } + + public static class EmojiPagerAdapter extends FragmentStatePagerAdapter + implements PagerSlidingTabStrip.CustomTabProvider + { + private Context context; + private List pages; + private EmojiSelectionListener listener; + + public EmojiPagerAdapter(@NonNull Context context, + @NonNull FragmentManager fm, + @NonNull List pages, + @Nullable EmojiSelectionListener listener) + { + super(fm); + this.context = context; + this.pages = pages; + this.listener = listener; + } + + @Override + public int getCount() { + return pages.size(); + } + + @Override public Fragment getItem(int i) { + return EmojiPageFragment.newInstance(pages.get(i), listener); + } + + @Override public View getCustomTabView(ViewGroup viewGroup, int i) { + ImageView image = new ImageView(context); + image.setImageResource(pages.get(i).getIconRes()); + return image; + } + } +} diff --git a/src/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java b/src/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java new file mode 100644 index 0000000000..5a7494f3e2 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java @@ -0,0 +1,38 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.content.Context; +import android.support.v7.widget.AppCompatEditText; +import android.util.AttributeSet; + +public class EmojiEditText extends AppCompatEditText { + public EmojiEditText(Context context) { + super(context); + init(); + } + + public EmojiEditText(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public EmojiEditText(Context context, AttributeSet attrs, + int defStyleAttr) + { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + setTransformationMethod(new EmojiTransformationMethod()); + } + + public void insertEmoji(int codePoint) { + final char[] chars = Character.toChars(codePoint); + final String text = new String(chars); + final int start = getSelectionStart(); + final int end = getSelectionEnd(); + + getText().replace(Math.min(start, end), Math.max(start, end), text, 0, text.length()); + setSelection(end + chars.length); + } +} diff --git a/src/org/thoughtcrime/securesms/components/emoji/EmojiPageFragment.java b/src/org/thoughtcrime/securesms/components/emoji/EmojiPageFragment.java new file mode 100644 index 0000000000..c9e7f33d39 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/emoji/EmojiPageFragment.java @@ -0,0 +1,116 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.BaseAdapter; +import android.widget.GridView; +import android.widget.ImageView; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.emoji.EmojiProvider.InvalidatingPageLoadedListener; + +public class EmojiPageFragment extends Fragment { + private static final String TAG = EmojiPageFragment.class.getSimpleName(); + + private EmojiPageModel model; + private EmojiSelectionListener listener; + + public static EmojiPageFragment newInstance(@NonNull EmojiPageModel model, + @Nullable EmojiSelectionListener listener) + { + EmojiPageFragment fragment = new EmojiPageFragment(); + fragment.setModel(model); + fragment.setEmojiSelectedListener(listener); + return fragment; + } + + @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) + { + final View view = inflater.inflate(R.layout.emoji_grid_layout, container, false); + final GridView grid = (GridView) view.findViewById(R.id.emoji); + grid.setColumnWidth(getResources().getDimensionPixelSize(R.dimen.emoji_drawer_size) + 2 * getResources().getDimensionPixelSize(R.dimen.emoji_drawer_item_padding)); + grid.setOnItemClickListener(new OnItemClickListener() { + @Override public void onItemClick(AdapterView parent, View view, int position, long id) { + model.onCodePointSelected((Integer)view.getTag()); + if (listener != null) listener.onEmojiSelected((Integer)view.getTag()); + } + }); + grid.setAdapter(new EmojiGridAdapter(getActivity(), model)); + return view; + } + + public void setModel(EmojiPageModel model) { + this.model = model; + } + + public void setEmojiSelectedListener(EmojiSelectionListener listener) { + this.listener = listener; + } + + private static class EmojiGridAdapter extends BaseAdapter { + + protected final Context context; + private final int emojiSize; + private final EmojiPageModel model; + + public EmojiGridAdapter(Context context, EmojiPageModel model) { + this.context = context; + this.emojiSize = (int) context.getResources().getDimension(R.dimen.emoji_drawer_size); + this.model = model; + } + + @Override public int getCount() { + return model.getCodePoints().length; + } + + @Override + public Object getItem(int position) { + return null; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(final int position, final View convertView, final ViewGroup parent) { + final ImageView view; + final int pad = context.getResources().getDimensionPixelSize(R.dimen.emoji_drawer_item_padding); + if (convertView != null && convertView instanceof ImageView) { + view = (ImageView)convertView; + } else { + ImageView imageView = new ImageView(context); + imageView.setPadding(pad, pad, pad, pad); + imageView.setLayoutParams(new AbsListView.LayoutParams(emojiSize + 2 * pad, emojiSize + 2 * pad)); + view = imageView; + } + + final Integer unicodeTag = model.getCodePoints()[position]; + final EmojiProvider provider = EmojiProvider.getInstance(context); + final Drawable drawable = provider.getEmojiDrawable(unicodeTag, + EmojiProvider.EMOJI_HUGE, + new InvalidatingPageLoadedListener(view)); + + view.setImageDrawable(drawable); + view.setPadding(pad, pad, pad, pad); + view.setTag(unicodeTag); + return view; + } + } + + public interface EmojiSelectionListener { + void onEmojiSelected(int emojiCode); + } +} diff --git a/src/org/thoughtcrime/securesms/components/emoji/EmojiPageModel.java b/src/org/thoughtcrime/securesms/components/emoji/EmojiPageModel.java new file mode 100644 index 0000000000..efe0b4eee3 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/emoji/EmojiPageModel.java @@ -0,0 +1,7 @@ +package org.thoughtcrime.securesms.components.emoji; + +public interface EmojiPageModel { + int getIconRes(); + int[] getCodePoints(); + void onCodePointSelected(int codePoint); +} diff --git a/src/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java b/src/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java new file mode 100644 index 0000000000..f0982728c9 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java @@ -0,0 +1,257 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.style.ImageSpan; +import android.util.Log; +import android.util.SparseArray; +import android.view.View; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.BitmapDecodingException; +import org.thoughtcrime.securesms.util.BitmapUtil; +import org.thoughtcrime.securesms.util.ResUtil; +import org.thoughtcrime.securesms.util.Util; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.SoftReference; +import java.util.Arrays; +import java.util.concurrent.ExecutorService; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class EmojiProvider { + private static final String TAG = EmojiProvider.class.getSimpleName(); + private static final ExecutorService executor = Util.newSingleThreadedLifoExecutor(); + private static volatile EmojiProvider instance = null; + private static final SparseArray> bitmaps = new SparseArray<>(); + + private final SparseArray offsets = new SparseArray<>(); + + @SuppressWarnings("MalformedRegex") + // 0x20a0-0x32ff 0x1f00-0x1fff 0xfe4e5-0xfe4ee + // |==== misc ====||======== emoticons ========||========= flags ==========| + private static final Pattern EMOJI_RANGE = Pattern.compile("[\\u20a0-\\u32ff\\ud83c\\udc00-\\ud83d\\udeff\\udbb9\\udce5-\\udbb9\\udcee]"); + + public static final double EMOJI_HUGE = 1.00; + public static final double EMOJI_LARGE = 0.75; + public static final double EMOJI_SMALL = 0.60; + public static final int EMOJI_RAW_SIZE = 128; + public static final int EMOJI_PER_ROW = 16; + + private final Context context; + private final int bigDrawSize; + private final int[] pages; + + public static EmojiProvider getInstance(Context context) { + if (instance == null) { + synchronized (EmojiProvider.class) { + if (instance == null) { + instance = new EmojiProvider(context); + } + } + } + return instance; + } + + private EmojiProvider(Context context) { + this.context = context.getApplicationContext(); + this.bigDrawSize = context.getResources().getDimensionPixelSize(R.dimen.emoji_drawer_size); + this.pages = ResUtil.getResourceIds(context, R.array.emoji_categories); + for (int i = 0; i < pages.length; i++) { + final int[] page = context.getResources().getIntArray(pages[i]); + for (int j = 0; j < page.length; j++) { + offsets.put(page[j], new DrawInfo(i, j)); + } + } + } + + private void preloadPage(final int page, final PageLoadedListener pageLoadListener) { + executor.submit(new Runnable() { + @Override + public void run() { + try { + loadPage(page); + if (pageLoadListener != null) { + Log.w(TAG, "onPageLoaded("+page+")"); + pageLoadListener.onPageLoaded(); + } + } catch (IOException ioe) { + Log.w(TAG, ioe); + } + } + }); + } + + private void loadPage(int page) throws IOException { + if (page < 0 || page >= pages.length) { + throw new IndexOutOfBoundsException("can't load page that doesn't exist"); + } + + if (bitmaps.get(page) != null && bitmaps.get(page).get() != null) return; + + try { + final String file = "emoji_" + page + "_wrapped.png"; + final InputStream measureStream = context.getAssets().open(file); + final InputStream bitmapStream = context.getAssets().open(file); + final Bitmap bitmap = BitmapUtil.createScaledBitmap(measureStream, bitmapStream, (float) bigDrawSize / (float) EMOJI_RAW_SIZE); + bitmaps.put(page, new SoftReference<>(bitmap)); + } catch (IOException ioe) { + Log.w(TAG, ioe); + throw ioe; + } catch (BitmapDecodingException bde) { + Log.w(TAG, bde); + throw new AssertionError("emoji sprite asset is corrupted or android decoding is broken"); + } + } + + public CharSequence emojify(CharSequence text, PageLoadedListener pageLoadedListener) { + return emojify(text, EMOJI_LARGE, pageLoadedListener); + } + + public CharSequence emojify(CharSequence text, double size, PageLoadedListener pageLoadedListener) { + Matcher matches = EMOJI_RANGE.matcher(text); + SpannableStringBuilder builder = new SpannableStringBuilder(text); + + while (matches.find()) { + int codePoint = matches.group().codePointAt(0); + Drawable drawable = getEmojiDrawable(codePoint, size, pageLoadedListener); + if (drawable != null) { + ImageSpan imageSpan = new ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM); + char[] chars = new char[matches.end() - matches.start()]; + Arrays.fill(chars, ' '); + builder.setSpan(imageSpan, matches.start(), matches.end(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + return builder; + } + + public Drawable getEmojiDrawable(int emojiCode, double size, PageLoadedListener pageLoadedListener) { + return getEmojiDrawable(offsets.get(emojiCode), size, pageLoadedListener); + } + + private Drawable getEmojiDrawable(DrawInfo drawInfo, double size, PageLoadedListener pageLoadedListener) { + if (drawInfo == null) { + return null; + } + final Drawable drawable = new EmojiDrawable(drawInfo, bigDrawSize); + drawable.setBounds(0, 0, (int)((double)bigDrawSize * size), (int)((double)bigDrawSize * size)); + if (bitmaps.get(drawInfo.page) == null || bitmaps.get(drawInfo.page).get() == null) { + preloadPage(drawInfo.page, pageLoadedListener); + } + return drawable; + } + + public static class EmojiDrawable extends Drawable { + private final int index; + private final int page; + private final int emojiSize; + private static final Paint paint; + private Bitmap bmp; + + static { + paint = new Paint(); + paint.setFilterBitmap(true); + } + + public EmojiDrawable(DrawInfo info, int emojiSize) { + this.index = info.index; + this.page = info.page; + this.emojiSize = emojiSize; + } + + @Override + public void draw(Canvas canvas) { + if (bitmaps.get(page) == null || bitmaps.get(page).get() == null) { + Log.w(TAG, "bitmap for this page was null"); + return; + } + if (bmp == null) { + bmp = bitmaps.get(page).get(); + } + + Rect b = copyBounds(); + + final int row = index / EMOJI_PER_ROW; + final int row_index = index % EMOJI_PER_ROW; + + canvas.drawBitmap(bmp, + new Rect(row_index * emojiSize, + row * emojiSize, + (row_index + 1) * emojiSize, + (row + 1) * emojiSize), + b, + paint); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public void setAlpha(int alpha) { } + + @Override + public void setColorFilter(ColorFilter cf) { } + + @Override + public String toString() { + return "EmojiDrawable{" + + "page=" + page + + ", index=" + index + + '}'; + } + } + + public static class InvalidatingPageLoadedListener implements PageLoadedListener { + private final View view; + + public InvalidatingPageLoadedListener(final View view) { + this.view = view; + } + + @Override + public void onPageLoaded() { + view.postInvalidate(); + } + + @Override + public String toString() { + return "InvalidatingPageLoadedListener{}"; + } + } + + class DrawInfo { + int page; + int index; + + public DrawInfo(final int page, final int index) { + this.page = page; + this.index = index; + } + + @Override + public String toString() { + return "DrawInfo{" + + "page=" + page + + ", index=" + index + + '}'; + } + } + + interface PageLoadedListener { + void onPageLoaded(); + } +} diff --git a/src/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java b/src/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java new file mode 100644 index 0000000000..e304a0c2ef --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java @@ -0,0 +1,34 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build.VERSION_CODES; +import android.util.AttributeSet; +import android.widget.TextView; + +public class EmojiTextView extends TextView { + public EmojiTextView(Context context) { + super(context); + init(); + } + + public EmojiTextView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + @TargetApi(VERSION_CODES.LOLLIPOP) + public EmojiTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + private void init() { + setTransformationMethod(new EmojiTransformationMethod()); + } +} diff --git a/src/org/thoughtcrime/securesms/components/EmojiToggle.java b/src/org/thoughtcrime/securesms/components/emoji/EmojiToggle.java similarity index 97% rename from src/org/thoughtcrime/securesms/components/EmojiToggle.java rename to src/org/thoughtcrime/securesms/components/emoji/EmojiToggle.java index 5d51b8f166..2d969a1b9f 100644 --- a/src/org/thoughtcrime/securesms/components/EmojiToggle.java +++ b/src/org/thoughtcrime/securesms/components/emoji/EmojiToggle.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.components; +package org.thoughtcrime.securesms.components.emoji; import android.content.Context; diff --git a/src/org/thoughtcrime/securesms/components/emoji/EmojiTransformationMethod.java b/src/org/thoughtcrime/securesms/components/emoji/EmojiTransformationMethod.java new file mode 100644 index 0000000000..e3f57db74b --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/emoji/EmojiTransformationMethod.java @@ -0,0 +1,17 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.graphics.Rect; +import android.text.method.TransformationMethod; +import android.view.View; + +import org.thoughtcrime.securesms.components.emoji.EmojiProvider.InvalidatingPageLoadedListener; + +class EmojiTransformationMethod implements TransformationMethod { + + @Override public CharSequence getTransformation(CharSequence source, View view) { + return EmojiProvider.getInstance(view.getContext()).emojify(source, EmojiProvider.EMOJI_SMALL, new InvalidatingPageLoadedListener(view)); + } + + @Override public void onFocusChanged(View view, CharSequence sourceText, boolean focused, + int direction, Rect previouslyFocusedRect) { } +} diff --git a/src/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java b/src/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java new file mode 100644 index 0000000000..fc7b1e2cd0 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java @@ -0,0 +1,111 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +import com.fasterxml.jackson.databind.type.CollectionType; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.JsonUtils; + +import java.io.IOException; +import java.util.Iterator; +import java.util.LinkedHashSet; + +public class RecentEmojiPageModel implements EmojiPageModel { + private static final String TAG = RecentEmojiPageModel.class.getSimpleName(); + private static final String EMOJI_LRU_PREFERENCE = "pref_recent_emoji"; + private static final int EMOJI_LRU_SIZE = 50; + + private final SharedPreferences prefs; + private final LinkedHashSet recentlyUsed; + + public RecentEmojiPageModel(Context context) { + this.prefs = PreferenceManager.getDefaultSharedPreferences(context); + this.recentlyUsed = getPersistedCache(); + } + + private LinkedHashSet getPersistedCache() { + String serialized = prefs.getString(EMOJI_LRU_PREFERENCE, "[]"); + LinkedHashSet recentlyUsedStrings; + try { + CollectionType collectionType = TypeFactory.defaultInstance() + .constructCollectionType(LinkedHashSet.class, String.class); + recentlyUsedStrings = JsonUtils.getMapper().readValue(serialized, collectionType); + return fromHexString(recentlyUsedStrings); + } catch (IOException e) { + Log.w(TAG, e); + return new LinkedHashSet<>(); + } + } + + @Override public int getIconRes() { + return R.drawable.emoji_category_recent; + } + + @Override public int[] getCodePoints() { + return toPrimitiveArray(recentlyUsed); + } + + @Override public void onCodePointSelected(int codePoint) { + recentlyUsed.remove(codePoint); + recentlyUsed.add(codePoint); + + if (recentlyUsed.size() > EMOJI_LRU_SIZE) { + Iterator iterator = recentlyUsed.iterator(); + iterator.next(); + iterator.remove(); + } + + final LinkedHashSet latestRecentlyUsed = new LinkedHashSet<>(recentlyUsed); + new AsyncTask() { + + @Override + protected Void doInBackground(Void... params) { + try { + String serialized = JsonUtils.toJson(toHexString(latestRecentlyUsed)); + prefs.edit() + .putString(EMOJI_LRU_PREFERENCE, serialized) + .apply(); + } catch (IOException e) { + Log.w(TAG, e); + } + + return null; + } + }.execute(); + } + + private LinkedHashSet fromHexString(@Nullable LinkedHashSet stringSet) { + final LinkedHashSet integerSet = new LinkedHashSet<>(stringSet != null ? stringSet.size() : 0); + if (stringSet != null) { + for (String hexString : stringSet) { + integerSet.add(Integer.valueOf(hexString, 16)); + } + } + return integerSet; + } + + private LinkedHashSet toHexString(@NonNull LinkedHashSet integerSet) { + final LinkedHashSet stringSet = new LinkedHashSet<>(integerSet.size()); + for (Integer integer : integerSet) { + stringSet.add(Integer.toHexString(integer)); + } + return stringSet; + } + + private int[] toPrimitiveArray(@NonNull LinkedHashSet integerSet) { + int[] ints = new int[integerSet.size()]; + int i = 0; + for (Integer integer : integerSet) { + ints[i++] = integer; + } + return ints; + } +} diff --git a/src/org/thoughtcrime/securesms/components/emoji/StaticEmojiPageModel.java b/src/org/thoughtcrime/securesms/components/emoji/StaticEmojiPageModel.java new file mode 100644 index 0000000000..3175c61b4a --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/emoji/StaticEmojiPageModel.java @@ -0,0 +1,24 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; + +public class StaticEmojiPageModel implements EmojiPageModel { + @DrawableRes private final int icon; + @NonNull private final int[] codePoints; + + public StaticEmojiPageModel(@DrawableRes int icon, @NonNull int[] codePoints) { + this.icon = icon; + this.codePoints = codePoints; + } + + public int getIconRes() { + return icon; + } + + @NonNull public int[] getCodePoints() { + return codePoints; + } + + @Override public void onCodePointSelected(int codePoint) { } +} diff --git a/src/org/thoughtcrime/securesms/util/Emoji.java b/src/org/thoughtcrime/securesms/util/Emoji.java deleted file mode 100644 index e50b33b8e9..0000000000 --- a/src/org/thoughtcrime/securesms/util/Emoji.java +++ /dev/null @@ -1,474 +0,0 @@ -package org.thoughtcrime.securesms.util; - -import android.content.Context; -import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.ColorFilter; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.AsyncTask; -import android.os.Build; -import android.preference.PreferenceManager; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.style.ImageSpan; -import android.util.Log; -import android.util.Pair; -import android.util.SparseArray; -import android.view.View; - -import com.fasterxml.jackson.databind.type.TypeFactory; - -import org.thoughtcrime.securesms.R; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.concurrent.ExecutorService; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class Emoji { - - private static final String TAG = Emoji.class.getSimpleName(); - - private static ExecutorService executor = Util.newSingleThreadedLifoExecutor(); - - public static final int[][] PAGES = { - { - 0x263a, 0x1f60a, 0x1f600, 0x1f601, 0x1f602, 0x1f603, 0x1f604, 0x1f605, - 0x1f606, 0x1f607, 0x1f608, 0x1f609, 0x1f62f, 0x1f610, 0x1f611, 0x1f615, - 0x1f620, 0x1f62c, 0x1f621, 0x1f622, 0x1f634, 0x1f62e, 0x1f623, 0x1f624, - 0x1f625, 0x1f626, 0x1f627, 0x1f628, 0x1f629, 0x1f630, 0x1f61f, 0x1f631, - 0x1f632, 0x1f633, 0x1f635, 0x1f636, 0x1f637, 0x1f61e, 0x1f612, 0x1f60d, - 0x1f61b, 0x1f61c, 0x1f61d, 0x1f60b, 0x1f617, 0x1f619, 0x1f618, 0x1f61a, - 0x1f60e, 0x1f62d, 0x1f60c, 0x1f616, 0x1f614, 0x1f62a, 0x1f60f, 0x1f613, - 0x1f62b, 0x1f64b, 0x1f64c, 0x1f64d, 0x1f645, 0x1f646, 0x1f647, 0x1f64e, - 0x1f64f, 0x1f63a, 0x1f63c, 0x1f638, 0x1f639, 0x1f63b, 0x1f63d, 0x1f63f, - 0x1f63e, 0x1f640, 0x1f648, 0x1f649, 0x1f64a, 0x1f4a9, 0x1f476, 0x1f466, - 0x1f467, 0x1f468, 0x1f469, 0x1f474, 0x1f475, 0x1f48f, 0x1f491, 0x1f46a, - 0x1f46b, 0x1f46c, 0x1f46d, 0x1f464, 0x1f465, 0x1f46e, 0x1f477, 0x1f481, - 0x1f482, 0x1f46f, 0x1f470, 0x1f478, 0x1f385, 0x1f47c, 0x1f471, 0x1f472, - 0x1f473, 0x1f483, 0x1f486, 0x1f487, 0x1f485, 0x1f47b, 0x1f479, 0x1f47a, - 0x1f47d, 0x1f47e, 0x1f47f, 0x1f480, 0x1f4aa, 0x1f440, 0x1f442, 0x1f443, - 0x1f463, 0x1f444, 0x1f445, 0x1f48b, 0x2764, 0x1f499, 0x1f49a, 0x1f49b, - 0x1f49c, 0x1f493, 0x1f494, 0x1f495, 0x1f496, 0x1f497, 0x1f498, 0x1f49d, - 0x1f49e, 0x1f49f, 0x1f44d, 0x1f44e, 0x1f44c, 0x270a, 0x270c, 0x270b, - 0x1f44a, 0x261d, 0x1f446, 0x1f447, 0x1f448, 0x1f449, 0x1f44b, 0x1f44f, - 0x1f450 - }, - { - 0x1f530, 0x1f484, 0x1f45e, 0x1f45f, 0x1f451, 0x1f452, 0x1f3a9, 0x1f393, - 0x1f453, 0x231a, 0x1f454, 0x1f455, 0x1f456, 0x1f457, 0x1f458, 0x1f459, - 0x1f460, 0x1f461, 0x1f462, 0x1f45a, 0x1f45c, 0x1f4bc, 0x1f392, 0x1f45d, - 0x1f45b, 0x1f4b0, 0x1f4b3, 0x1f4b2, 0x1f4b5, 0x1f4b4, 0x1f4b6, 0x1f4b7, - 0x1f4b8, 0x1f4b1, 0x1f4b9, 0x1f52b, 0x1f52a, 0x1f4a3, 0x1f489, 0x1f48a, - 0x1f6ac, 0x1f514, 0x1f515, 0x1f6aa, 0x1f52c, 0x1f52d, 0x1f52e, 0x1f526, - 0x1f50b, 0x1f50c, 0x1f4dc, 0x1f4d7, 0x1f4d8, 0x1f4d9, 0x1f4da, 0x1f4d4, - 0x1f4d2, 0x1f4d1, 0x1f4d3, 0x1f4d5, 0x1f4d6, 0x1f4f0, 0x1f4db, 0x1f383, - 0x1f384, 0x1f380, 0x1f381, 0x1f382, 0x1f388, 0x1f386, 0x1f387, 0x1f389, - 0x1f38a, 0x1f38d, 0x1f38f, 0x1f38c, 0x1f390, 0x1f38b, 0x1f38e, 0x1f4f1, - 0x1f4f2, 0x1f4df, 0x260e, 0x1f4de, 0x1f4e0, 0x1f4e6, 0x2709, 0x1f4e8, - 0x1f4e9, 0x1f4ea, 0x1f4eb, 0x1f4ed, 0x1f4ec, 0x1f4ee, 0x1f4e4, 0x1f4e5, - 0x1f4ef, 0x1f4e2, 0x1f4e3, 0x1f4e1, 0x1f4ac, 0x1f4ad, 0x2712, 0x270f, - 0x1f4dd, 0x1f4cf, 0x1f4d0, 0x1f4cd, 0x1f4cc, 0x1f4ce, 0x2702, 0x1f4ba, - 0x1f4bb, 0x1f4bd, 0x1f4be, 0x1f4bf, 0x1f4c6, 0x1f4c5, 0x1f4c7, 0x1f4cb, - 0x1f4c1, 0x1f4c2, 0x1f4c3, 0x1f4c4, 0x1f4ca, 0x1f4c8, 0x1f4c9, 0x26fa, - 0x1f3a1, 0x1f3a2, 0x1f3a0, 0x1f3aa, 0x1f3a8, 0x1f3ac, 0x1f3a5, 0x1f4f7, - 0x1f4f9, 0x1f3a6, 0x1f3ad, 0x1f3ab, 0x1f3ae, 0x1f3b2, 0x1f3b0, 0x1f0cf, - 0x1f3b4, 0x1f004, 0x1f3af, 0x1f4fa, 0x1f4fb, 0x1f4c0, 0x1f4fc, 0x1f3a7, - 0x1f3a4, 0x1f3b5, 0x1f3b6, 0x1f3bc, 0x1f3bb, 0x1f3b9, 0x1f3b7, 0x1f3ba, - 0x1f3b8, 0x303d - }, - { - 0x1f415, 0x1f436, 0x1f429, 0x1f408, 0x1f431, 0x1f400, 0x1f401, 0x1f42d, - 0x1f439, 0x1f422, 0x1f407, 0x1f430, 0x1f413, 0x1f414, 0x1f423, 0x1f424, - 0x1f425, 0x1f426, 0x1f40f, 0x1f411, 0x1f410, 0x1f43a, 0x1f403, 0x1f402, - 0x1f404, 0x1f42e, 0x1f434, 0x1f417, 0x1f416, 0x1f437, 0x1f43d, 0x1f438, - 0x1f40d, 0x1f43c, 0x1f427, 0x1f418, 0x1f428, 0x1f412, 0x1f435, 0x1f406, - 0x1f42f, 0x1f43b, 0x1f42b, 0x1f42a, 0x1f40a, 0x1f433, 0x1f40b, 0x1f41f, - 0x1f420, 0x1f421, 0x1f419, 0x1f41a, 0x1f42c, 0x1f40c, 0x1f41b, 0x1f41c, - 0x1f41d, 0x1f41e, 0x1f432, 0x1f409, 0x1f43e, 0x1f378, 0x1f37a, 0x1f37b, - 0x1f377, 0x1f379, 0x1f376, 0x2615, 0x1f375, 0x1f37c, 0x1f374, 0x1f368, - 0x1f367, 0x1f366, 0x1f369, 0x1f370, 0x1f36a, 0x1f36b, 0x1f36c, 0x1f36d, - 0x1f36e, 0x1f36f, 0x1f373, 0x1f354, 0x1f35f, 0x1f35d, 0x1f355, 0x1f356, - 0x1f357, 0x1f364, 0x1f363, 0x1f371, 0x1f35e, 0x1f35c, 0x1f359, 0x1f35a, - 0x1f35b, 0x1f372, 0x1f365, 0x1f362, 0x1f361, 0x1f358, 0x1f360, 0x1f34c, - 0x1f34e, 0x1f34f, 0x1f34a, 0x1f34b, 0x1f344, 0x1f345, 0x1f346, 0x1f347, - 0x1f348, 0x1f349, 0x1f350, 0x1f351, 0x1f352, 0x1f353, 0x1f34d, 0x1f330, - 0x1f331, 0x1f332, 0x1f333, 0x1f334, 0x1f335, 0x1f337, 0x1f338, 0x1f339, - 0x1f340, 0x1f341, 0x1f342, 0x1f343, 0x1f33a, 0x1f33b, 0x1f33c, 0x1f33d, - 0x1f33e, 0x1f33f, 0x2600, 0x1f308, 0x26c5, 0x2601, 0x1f301, 0x1f302, - 0x2614, 0x1f4a7, 0x26a1, 0x1f300, 0x2744, 0x26c4, 0x1f319, 0x1f31e, - 0x1f31d, 0x1f31a, 0x1f31b, 0x1f31c, 0x1f311, 0x1f312, 0x1f313, 0x1f314, - 0x1f315, 0x1f316, 0x1f317, 0x1f318, 0x1f391, 0x1f304, 0x1f305, 0x1f307, - 0x1f306, 0x1f303, 0x1f30c, 0x1f309, 0x1f30a, 0x1f30b, 0x1f30e, 0x1f30f, - 0x1f30d, 0x1f310 - }, - { - 0x1f3e0, 0x1f3e1, 0x1f3e2, 0x1f3e3, 0x1f3e4, 0x1f3e5, 0x1f3e6, 0x1f3e7, - 0x1f3e8, 0x1f3e9, 0x1f3ea, 0x1f3eb, 0x26ea, 0x26f2, 0x1f3ec, 0x1f3ef, - 0x1f3f0, 0x1f3ed, 0x1f5fb, 0x1f5fc, 0x1f5fd, 0x1f5fe, 0x1f5ff, 0x2693, - 0x1f3ee, 0x1f488, 0x1f527, 0x1f528, 0x1f529, 0x1f6bf, 0x1f6c1, 0x1f6c0, - 0x1f6bd, 0x1f6be, 0x1f3bd, 0x1f3a3, 0x1f3b1, 0x1f3b3, 0x26be, 0x26f3, - 0x1f3be, 0x26bd, 0x1f3bf, 0x1f3c0, 0x1f3c1, 0x1f3c2, 0x1f3c3, 0x1f3c4, - 0x1f3c6, 0x1f3c7, 0x1f40e, 0x1f3c8, 0x1f3c9, 0x1f3ca, 0x1f682, 0x1f683, - 0x1f684, 0x1f685, 0x1f686, 0x1f687, 0x24c2, 0x1f688, 0x1f68a, 0x1f68b, - 0x1f68c, 0x1f68d, 0x1f68e, 0x1f68f, 0x1f690, 0x1f691, 0x1f692, 0x1f693, - 0x1f694, 0x1f695, 0x1f696, 0x1f697, 0x1f698, 0x1f699, 0x1f69a, 0x1f69b, - 0x1f69c, 0x1f69d, 0x1f69e, 0x1f69f, 0x1f6a0, 0x1f6a1, 0x1f6a2, 0x1f6a3, - 0x1f681, 0x2708, 0x1f6c2, 0x1f6c3, 0x1f6c4, 0x1f6c5, 0x26f5, 0x1f6b2, - 0x1f6b3, 0x1f6b4, 0x1f6b5, 0x1f6b7, 0x1f6b8, 0x1f689, 0x1f680, 0x1f6a4, - 0x1f6b6, 0x26fd, 0x1f17f, 0x1f6a5, 0x1f6a6, 0x1f6a7, 0x1f6a8, 0x2668, - 0x1f48c, 0x1f48d, 0x1f48e, 0x1f490, 0x1f492, 0xfe4e5, 0xfe4e6, 0xfe4e7, - 0xfe4e8, 0xfe4e9, 0xfe4ea, 0xfe4eb, 0xfe4ec, 0xfe4ed, 0xfe4ee - }, - { - 0x1f51d, 0x1f519, 0x1f51b, 0x1f51c, 0x1f51a, 0x23f3, 0x231b, 0x23f0, - 0x2648, 0x2649, 0x264a, 0x264b, 0x264c, 0x264d, 0x264e, 0x264f, - 0x2650, 0x2651, 0x2652, 0x2653, 0x26ce, 0x1f531, 0x1f52f, 0x1f6bb, - 0x1f6ae, 0x1f6af, 0x1f6b0, 0x1f6b1, 0x1f170, 0x1f171, 0x1f18e, 0x1f17e, - 0x1f4ae, 0x1f4af, 0x1f520, 0x1f521, 0x1f522, 0x1f523, 0x1f524, 0x27bf, - 0x1f4f6, 0x1f4f3, 0x1f4f4, 0x1f4f5, 0x1f6b9, 0x1f6ba, 0x1f6bc, 0x267f, - 0x267b, 0x1f6ad, 0x1f6a9, 0x26a0, 0x1f201, 0x1f51e, 0x26d4, 0x1f192, - 0x1f197, 0x1f195, 0x1f198, 0x1f199, 0x1f193, 0x1f196, 0x1f19a, 0x1f232, - 0x1f233, 0x1f234, 0x1f235, 0x1f236, 0x1f237, 0x1f238, 0x1f239, 0x1f202, - 0x1f23a, 0x1f250, 0x1f251, 0x3299, 0x00ae, 0x00a9, 0x2122, 0x1f21a, - 0x1f22f, 0x3297, 0x2b55, 0x274c, 0x274e, 0x2139, 0x1f6ab, 0x2705, - 0x2714, 0x1f517, 0x2734, 0x2733, 0x2795, 0x2796, 0x2716, 0x2797, - 0x1f4a0, 0x1f4a1, 0x1f4a4, 0x1f4a2, 0x1f525, 0x1f4a5, 0x1f4a8, 0x1f4a6, - 0x1f4ab, 0x1f55b, 0x1f567, 0x1f550, 0x1f55c, 0x1f551, 0x1f55d, 0x1f552, - 0x1f55e, 0x1f553, 0x1f55f, 0x1f554, 0x1f560, 0x1f555, 0x1f561, 0x1f556, - 0x1f562, 0x1f557, 0x1f563, 0x1f558, 0x1f564, 0x1f559, 0x1f565, 0x1f55a, - 0x1f566, 0x2195, 0x2b06, 0x2197, 0x27a1, 0x2198, 0x2b07, 0x2199, - 0x2b05, 0x2196, 0x2194, 0x2934, 0x2935, 0x23ea, 0x23eb, 0x23ec, - 0x23e9, 0x25c0, 0x25b6, 0x1f53d, 0x1f53c, 0x2747, 0x2728, 0x1f534, - 0x1f535, 0x26aa, 0x26ab, 0x1f533, 0x1f532, 0x2b50, 0x1f31f, 0x1f320, - 0x25ab, 0x25aa, 0x25fd, 0x25fe, 0x25fb, 0x25fc, 0x2b1c, 0x2b1b, - 0x1f538, 0x1f539, 0x1f536, 0x1f537, 0x1f53a, 0x1f53b, 0x1f51f, /*0x20e3,*/ - 0x2754, 0x2753, 0x2755, 0x2757, 0x203c, 0x2049, 0x3030, 0x27b0, - 0x2660, 0x2665, 0x2663, 0x2666, 0x1f194, 0x1f511, 0x21a9, 0x1f191, - 0x1f50d, 0x1f512, 0x1f513, 0x21aa, 0x1f510, 0x2611, 0x1f518, 0x1f50e, - 0x1f516, 0x1f50f, 0x1f503, 0x1f500, 0x1f501, 0x1f502, 0x1f504, 0x1f4e7, - 0x1f505, 0x1f506, 0x1f507, 0x1f508, 0x1f509, 0x1f50a - } - }; - - private static final SparseArray offsets; - - static { - offsets = new SparseArray(); - for (int i = 0; i < PAGES.length; i++) { - for (int j = 0; j < PAGES[i].length; j++) { - offsets.put(PAGES[i][j], new DrawInfo(i, j)); - } - } - } - - private static Bitmap[] bitmaps = new Bitmap[PAGES.length]; - - private static Emoji instance = null; - - public synchronized static Emoji getInstance(Context context) { - if (instance == null) { - instance = new Emoji(context); - } - - return instance; - } - - @SuppressWarnings("MalformedRegex") - // 0x20a0-0x32ff 0x1f00-0x1fff 0xfe4e5-0xfe4ee - // |==== misc ====||======== emoticons ========||========= flags ==========| - private static final Pattern EMOJI_RANGE = Pattern.compile("[\\u20a0-\\u32ff\\ud83c\\udc00-\\ud83d\\udeff\\udbb9\\udce5-\\udbb9\\udcee]"); - - public static final double EMOJI_HUGE = 1.00; - public static final double EMOJI_LARGE = 0.75; - public static final double EMOJI_SMALL = 0.50; - public static final int EMOJI_RAW_SIZE = 128; - public static final int EMOJI_PER_ROW = 16; - - private final Context context; - private final int bigDrawSize; - - private Emoji(Context context) { - this.context = context.getApplicationContext(); - this.bigDrawSize = context.getResources().getDimensionPixelSize(R.dimen.emoji_drawer_size); - } - - private void preloadPage(final int page, final PageLoadedListener pageLoadListener) { - executor.submit(new Runnable() { - @Override - public void run() { - try { - loadPage(page); - if (pageLoadListener != null) pageLoadListener.onPageLoaded(); - } catch (IOException ioe) { - Log.w("Emoji", ioe); - } - } - }); - } - - private void loadPage(int page) throws IOException { - if (page < 0 || page >= PAGES.length) { - throw new IndexOutOfBoundsException("can't load page that doesn't exist"); - } - - if (bitmaps[page] != null) return; - - try { - final String file = "emoji_" + page + "_wrapped.png"; - final InputStream measureStream = context.getAssets().open(file); - final InputStream bitmapStream = context.getAssets().open(file); - - bitmaps[page] = BitmapUtil.createScaledBitmap(measureStream, bitmapStream, (float) bigDrawSize / (float) EMOJI_RAW_SIZE); - } catch (IOException ioe) { - Log.w("Emoji", ioe); - throw ioe; - } catch (BitmapDecodingException bde) { - Log.w("Emoji", bde); - throw new AssertionError("emoji sprite asset is corrupted or android decoding is broken"); - } - } - - public SpannableString emojify(String text, PageLoadedListener pageLoadedListener) { - return emojify(new SpannableString(text), pageLoadedListener); - } - - public SpannableString emojify(SpannableString text, PageLoadedListener pageLoadedListener) { - return emojify(text, EMOJI_LARGE, pageLoadedListener); - } - - public SpannableString emojify(SpannableString text, double size, PageLoadedListener pageLoadedListener) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) return text; - - Matcher matches = EMOJI_RANGE.matcher(text); - - while (matches.find()) { - String resource = Integer.toHexString(matches.group().codePointAt(0)); - - Drawable drawable = getEmojiDrawable(resource, size, pageLoadedListener); - if (drawable != null) { - ImageSpan imageSpan = new ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM); - text.setSpan(imageSpan, matches.start(), matches.end(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - - return text; - } - - public Pair getRecentlyUsed(int position, double size, PageLoadedListener pageLoadedListener) { - String[] recentlyUsed = EmojiLRU.getRecentlyUsed(context); - String code = recentlyUsed[recentlyUsed.length - 1 - position]; - return new Pair(Integer.parseInt(code, 16), getEmojiDrawable(code, size, pageLoadedListener)); - } - - public void setRecentlyUsed(String emojiCode) { - EmojiLRU.putRecentlyUsed(context, emojiCode); - } - - public int getRecentlyUsedAssetCount() { - return EmojiLRU.getRecentlyUsedCount(context); - } - - public Drawable getEmojiDrawable(String emojiCode, double size, PageLoadedListener pageLoadedListener) { - return getEmojiDrawable(offsets.get(Integer.parseInt(emojiCode, 16)), size, pageLoadedListener); - } - - public Drawable getEmojiDrawable(DrawInfo drawInfo, double size, PageLoadedListener pageLoadedListener) { - if (drawInfo == null) { - return null; - } - final Drawable drawable = new EmojiDrawable(drawInfo, bigDrawSize); - drawable.setBounds(0, 0, (int) ((double) bigDrawSize * size), (int) ((double) bigDrawSize * size)); - if (bitmaps[drawInfo.page] == null) { - preloadPage(drawInfo.page, pageLoadedListener); - } - return drawable; - } - - private static class EmojiLRU { - private static SharedPreferences prefs = null; - private static LinkedHashSet recentlyUsed = null; - private static final String EMOJI_LRU_PREFERENCE = "pref_recent_emoji"; - private static final int EMOJI_LRU_SIZE = 50; - - private static void initializeCache(Context context) { - if (prefs == null) { - prefs = PreferenceManager.getDefaultSharedPreferences(context); - } - - String serialized = prefs.getString(EMOJI_LRU_PREFERENCE, "[]"); - - try { - recentlyUsed = JsonUtils.getMapper().readValue(serialized, TypeFactory.defaultInstance() - .constructCollectionType(LinkedHashSet.class, String.class)); - } catch (IOException e) { - Log.w(TAG, e); - recentlyUsed = new LinkedHashSet<>(); - } - } - - public static String[] getRecentlyUsed(Context context) { - if (recentlyUsed == null) initializeCache(context); - return recentlyUsed.toArray(new String[recentlyUsed.size()]); - } - - public static int getRecentlyUsedCount(Context context) { - if (recentlyUsed == null) initializeCache(context); - return recentlyUsed.size(); - } - - public static void putRecentlyUsed(Context context, String asset) { - if (recentlyUsed == null) initializeCache(context); - if (prefs == null) { - prefs = PreferenceManager.getDefaultSharedPreferences(context); - } - - recentlyUsed.remove(asset); - recentlyUsed.add(asset); - - if (recentlyUsed.size() > EMOJI_LRU_SIZE) { - Iterator iterator = recentlyUsed.iterator(); - iterator.next(); - iterator.remove(); - } - - final LinkedHashSet latestRecentlyUsed = new LinkedHashSet(recentlyUsed); - new AsyncTask() { - - @Override - protected Void doInBackground(Void... params) { - try { - String serialized = JsonUtils.toJson(latestRecentlyUsed); - prefs.edit() - .putString(EMOJI_LRU_PREFERENCE, serialized) - .apply(); - } catch (IOException e) { - Log.w(TAG, e); - } - - return null; - } - }.execute(); - - } - } - - public static class EmojiDrawable extends Drawable { - private final int index; - private final int page; - private final int emojiSize; - private static final Paint paint; - private Bitmap bmp; - - static { - paint = new Paint(); - paint.setFilterBitmap(true); - } - - public EmojiDrawable(DrawInfo info, int emojiSize) { - this.index = info.index; - this.page = info.page; - this.emojiSize = emojiSize; - } - - @Override - public void draw(Canvas canvas) { - if (bitmaps[page] == null) { - Log.w("Emoji", "bitmap for this page was null"); - return; - } - if (bmp == null) { - bmp = bitmaps[page]; - } - - Rect b = copyBounds(); -// int cX = b.centerX(), cY = b.centerY(); -// b.left = cX - emojiSize / 2; -// b.right = cX + emojiSize / 2; -// b.top = cY - emojiSize / 2; -// b.bottom = cY + emojiSize / 2; - - final int row = index / EMOJI_PER_ROW; - final int row_index = index % EMOJI_PER_ROW; - - canvas.drawBitmap(bmp, - new Rect(row_index * emojiSize, - row * emojiSize, - (row_index + 1) * emojiSize, - (row + 1) * emojiSize), - b, - paint); - } - - @Override - public int getOpacity() { - return 0; - } - - @Override - public void setAlpha(int alpha) { } - - @Override - public void setColorFilter(ColorFilter cf) { } - - @Override - public String toString() { - return "EmojiDrawable{" + - "page=" + page + - ", index=" + index + - '}'; - } - } - - public static interface PageLoadedListener { - public void onPageLoaded(); - } - - public static class InvalidatingPageLoadedListener implements PageLoadedListener { - private final View view; - - public InvalidatingPageLoadedListener(final View view) { - this.view = view; - } - - @Override - public void onPageLoaded() { - view.post(new Runnable() { - @Override - public void run() { - view.invalidate(); - } - }); - } - - @Override - public String toString() { - return "InvalidatingPageLoadedListener{}"; - } - } - - public static class DrawInfo { - int page; - int index; - - public DrawInfo(final int page, final int index) { - this.page = page; - this.index = index; - } - - @Override - public String toString() { - return "DrawInfo{" + - "page=" + page + - ", index=" + index + - '}'; - } - } -} diff --git a/src/org/thoughtcrime/securesms/util/ResUtil.java b/src/org/thoughtcrime/securesms/util/ResUtil.java index 86910a5c6f..0984f10819 100644 --- a/src/org/thoughtcrime/securesms/util/ResUtil.java +++ b/src/org/thoughtcrime/securesms/util/ResUtil.java @@ -21,6 +21,7 @@ import android.content.Context; import android.content.res.Resources.Theme; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; +import android.support.annotation.ArrayRes; import android.support.annotation.AttrRes; import android.util.TypedValue; @@ -44,6 +45,16 @@ public class ResUtil { } public static Drawable getDrawable(Context c, @AttrRes int attr) { - return c.getResources().getDrawable(getDrawableRes(c, attr)); + return c.getResources().getDrawable(getDrawableRes(c, attr), c.getTheme()); + } + + public static int[] getResourceIds(Context c, @ArrayRes int array) { + final TypedArray typedArray = c.getResources().obtainTypedArray(array); + final int[] resourceIds = new int[typedArray.length()]; + for (int i = 0; i < typedArray.length(); i++) { + resourceIds[i] = typedArray.getResourceId(i, 0); + } + typedArray.recycle(); + return resourceIds; } }