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; } }